dsh-mobilecode 0.10.1 → 0.11.1

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 CHANGED
@@ -8,9 +8,10 @@ with embedded iOS Simulator / Android Emulator support) into
8
8
  It detects the mobile project in a directory (iOS / Android, Expo / React
9
9
  Native / native), builds, installs and launches the app on the booted
10
10
  simulator or emulator — and exposes the same to the agent through tools. The
11
- device screen has exactly **one** home in the panel: the **Live device**
12
- stream. (0.10.0 merged the old Android "Start server" preview into it —
13
- `serve-avd` is retired for the panel; `serve-sim` remains the iOS-only view.)
11
+ device screen has exactly **one** home in the panel: the **Device 1** live
12
+ stream (co-op adds a second pane, **Device 2**). (0.10.0 merged the old Android
13
+ "Start server" preview into it — `serve-avd` is retired for the panel;
14
+ `serve-sim` remains the iOS-only view.)
14
15
 
15
16
  ## Installation
16
17
 
@@ -32,12 +33,14 @@ If you find this useful, you can support development at [ko-fi.com/spix18](https
32
33
  drawer:
33
34
 
34
35
  - project directory input (persisted in localStorage) + Detect
35
- - platform pills — Android: attached-device status (its view is the Live
36
- device card); iOS: preview-server status
36
+ - platform pills — Android: attached-device status (its view is the Device 1
37
+ card); iOS: preview-server status. The iOS pill and card only render when the
38
+ host OS is macOS — on Windows/Linux there is no simulator to drive, so 0.11.0
39
+ stopped showing that dead affordance.
37
40
  - per platform: **Run app / Stop app**, build status/step/error and an
38
41
  expandable log tail. iOS additionally **Start/Stop server** with the
39
42
  embedded serve-sim iframe — Android's duplicated serve-avd preview was
40
- merged into the Live device card in 0.10.0
43
+ merged into the Device 1 card in 0.10.0
41
44
  - a Metro (bundler) status card
42
45
  - polls `GET /api/dsh-mobilecode` every 2 s while open
43
46
 
@@ -105,9 +108,10 @@ drawer:
105
108
  that just need to see the screen should still prefer `device_screen` /
106
109
  `device_ui_tree`.
107
110
 
108
- **Live device stream (the panel)**
111
+ **Live device stream (the panel — Device 1 / Device 2)**
109
112
 
110
- The Devices pane shows a real-time mirror of the attached device. It is produced
113
+ The Devices pane shows a real-time mirror of the attached device. The primary
114
+ card is **Device 1**; co-op docks a second, **Device 2**. It is produced
111
115
  **in-process** — no inner loopback port, no external helper:
112
116
 
113
117
  - ONE persistent `adb exec-out "while :; do screencap -p; done"` child **per
package/lib/client.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * - directory input (remembers the last one) + platform pills from detect,
8
8
  * - per platform: run/stop the app + build status (iOS only: start/stop
9
9
  * the serve-sim preview server with its embedded iframe),
10
- * - Live device card: the in-process stream 0.10.0 merged the Android
10
+ * - Device 1 card (the live stream): 0.10.0 merged the Android
11
11
  * "Start server" into it (serve-avd retired; picker ⏻ boot → POST /boot),
12
12
  * - build status, error and expandable log tail,
13
13
  * - polls GET /api/dsh-mobilecode every ~2s while open.
@@ -185,8 +185,17 @@ window.__ModuleLoader__.load({
185
185
  //#endregion
186
186
 
187
187
  const API_BASE = "/api/dsh-mobilecode";
188
- const PLATFORMS = ["ios", "android"];
188
+ // The panel shows cards for platforms the host can actually drive; the
189
+ // server reports its OS in info.os (iOS tooling is darwin-only).
190
+ const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
189
191
  const label = (p) => (p === "ios" ? "iOS" : "Android");
192
+ // Shared by both panes so Device 1 and Device 2 scale identically (0.11.0).
193
+ const stageSizeStyle = (m) => (m.mode === "pct"
194
+ ? { width: m.value + "%", maxWidth: "100%", maxHeight: "60vh" }
195
+ : m.mode === "px"
196
+ ? { width: m.value + "px", maxWidth: "100%", maxHeight: "60vh" }
197
+ : {});
198
+ const stageClassFor = (f) => "mc-live-stage" + (f !== "none" ? " " + f : "");
190
199
 
191
200
  function PanelController() {
192
201
  let panelOpen = false;
@@ -297,7 +306,7 @@ window.__ModuleLoader__.load({
297
306
 
298
307
  const server = (platform) => info?.servers?.find((s) => s.platform === platform);
299
308
  const build = (platform) => info?.builds?.find((b) => b.platform === platform);
300
- const platforms = info?.platforms?.length > 0 ? info.platforms : PLATFORMS;
309
+ const platforms = info?.platforms?.length > 0 ? info.platforms : fallbackPlatforms(info?.os);
301
310
 
302
311
  const statusClass = (status) =>
303
312
  status === "running" ? "on" : status === "starting" ? "warn" : status === "exited" ? "off" : "err";
@@ -337,13 +346,13 @@ window.__ModuleLoader__.load({
337
346
  error !== "" && h("div", { className: "mc-error" }, error),
338
347
  h("div", { className: "mc-pills" },
339
348
  platforms.map((platform) => {
340
- // 0.10.0: Android's "server" IS the Live device stream — the pill
349
+ // 0.10.0: Android's "server" IS the Device 1 stream — the pill
341
350
  // tracks attached devices, not a serve-avd process.
342
351
  const running = platform === "android"
343
352
  ? (info?.deviceCount ?? 0) > 0
344
353
  : server(platform)?.status === "running";
345
354
  return h("span", { key: platform, className: "mc-pill", "data-on": running ? "true" : undefined,
346
- title: platform === "android" ? (running ? "Android device attached — mirrored in Live device" : "No Android device attached") : "Preview server running" },
355
+ title: platform === "android" ? (running ? "Android device attached — mirrored in Device 1" : "No Android device attached") : "Preview server running" },
347
356
  h("span", { className: "mc-dot " + (running ? "on" : "off") }),
348
357
  label(platform),
349
358
  );
@@ -353,7 +362,7 @@ window.__ModuleLoader__.load({
353
362
  platforms.map((platform) => {
354
363
  const srv = server(platform);
355
364
  const bld = build(platform);
356
- // 0.10.0 merge: "Start server" (serve-avd iframe) and "Live device" were
365
+ // 0.10.0 merge: "Start server" (serve-avd iframe) and "Device 1" were
357
366
  // two views of the same screen. The in-process stream won; the Android
358
367
  // card keeps project controls only. iOS keeps serve-sim (its only view).
359
368
  const preview = platform === "ios";
@@ -365,7 +374,7 @@ window.__ModuleLoader__.load({
365
374
  ? h("button", { className: "mc-btn", disabled: busy !== "", onClick: () => act(platform + "-stop-server", "/stop", { platform }) },
366
375
  srv.status === "running" ? "Stop server" : "Server " + srv.status)
367
376
  : h("button", { className: "mc-btn primary", disabled: busy !== "", onClick: () => act(platform + "-start-server", "/start", { platform }) }, "Start server")),
368
- !preview && h("span", { className: "mc-live-cap" }, "screen: Live device ↑"),
377
+ !preview && h("span", { className: "mc-live-cap" }, "screen: Device 1 ↑"),
369
378
  ),
370
379
  preview && (srv?.url
371
380
  ? h("iframe", {
@@ -418,7 +427,7 @@ window.__ModuleLoader__.load({
418
427
  openLogs["bundler"] && h("pre", { className: "mc-log" }, info.bundler.log.join("\n")),
419
428
  ),
420
429
  info?.servers?.length === 0 && info?.builds?.length === 0 && (info?.deviceCount ?? 0) === 0 &&
421
- h("div", { className: "mc-empty" }, "Nothing running yet — pick a project directory and press Run app, or boot an AVD in Live device above."),
430
+ h("div", { className: "mc-empty" }, "Nothing running yet — pick a project directory and press Run app, or boot an AVD in Device 1 above."),
422
431
  ),
423
432
  h("div", { className: "mc-footer" },
424
433
  h("span", null, "dsh-mobilecode · polls every 2s"),
@@ -574,9 +583,9 @@ window.__ModuleLoader__.load({
574
583
  return { streamUrl, grant, reset, control, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
575
584
  }
576
585
 
577
- /** Live device stream: picker-popover header, SVG toolbar pill, device
586
+ /** Device 1 stream: picker-popover header, SVG toolbar pill, device
578
587
  * menu, quick sizes + frame styles, and tap/drag with duration. */
579
- function LiveDeviceCard({ coopOn, onToggleCoop, onSerial }) {
588
+ function LiveDeviceCard({ coopOn, onToggleCoop, onSerial, sizeMode, setSizeMode, frame, setFrame }) {
580
589
  const [devices, setDevices] = useState([]);
581
590
  const [avds, setAvds] = useState([]);
582
591
  const [serial, setSerial] = useState("");
@@ -585,9 +594,10 @@ window.__ModuleLoader__.load({
585
594
  const [menuOpen, setMenuOpen] = useState(false);
586
595
  const [still, setStill] = useState(null);
587
596
  const [view, setView] = useState("live"); // 'live' | 'still'
588
- const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
589
- const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
597
+ // sizeMode/frame live in LiveStreamSection (0.11.0) so both co-op
598
+ // panes scale identically Device 2 must never be smaller.
590
599
  const [booting, setBooting] = useState(""); // AVD name while POST /boot is in flight
600
+ const [powering, setPowering] = useState(""); // serial while POST /off is in flight
591
601
  const live = useLiveStream({ serial, onError: setError });
592
602
  const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
593
603
  const control = live.control;
@@ -653,14 +663,26 @@ window.__ModuleLoader__.load({
653
663
  setBooting("");
654
664
  }
655
665
  };
666
+ // 0.11.0 device power-off: graceful `adb emu kill` via POST /off.
667
+ // If we just killed the streamed device, drop back to the picker.
668
+ const powerOff = async (device) => {
669
+ setPowering(device); setError("");
670
+ try {
671
+ await streamPost("/off", { serial: device });
672
+ if (device === serial) { setSerial(""); live.reset(); }
673
+ await refreshDevices();
674
+ } catch (err) {
675
+ setError(err instanceof Error ? err.message : String(err));
676
+ await refreshDevices();
677
+ } finally {
678
+ setPowering("");
679
+ }
680
+ };
656
681
  const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
657
682
 
658
- const sizeStyle = sizeMode.mode === "pct"
659
- ? { width: sizeMode.value + "%", maxWidth: "100%", maxHeight: "60vh" }
660
- : sizeMode.mode === "px"
661
- ? { width: sizeMode.value + "px", maxWidth: "100%", maxHeight: "60vh" }
662
- : {};
663
- const stageClass = "mc-live-stage" + (frame !== "none" ? " " + frame : "");
683
+ const sizeStyle = stageSizeStyle(sizeMode);
684
+ const stageClass = stageClassFor(frame);
685
+ const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
664
686
  const toolbar = [
665
687
  { title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
666
688
  { title: "Home", icon: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5", run: () => control({ kind: "button", name: "home" }) },
@@ -687,7 +709,7 @@ window.__ModuleLoader__.load({
687
709
 
688
710
  return h("div", { className: "mc-card mc-live" },
689
711
  h("div", { className: "mc-card-head" },
690
- h("span", null, "Live device"),
712
+ h("span", null, "Device 1"),
691
713
  live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
692
714
  h("span", { className: "sp" }),
693
715
  h("div", { className: "mc-picker" },
@@ -696,20 +718,28 @@ window.__ModuleLoader__.load({
696
718
  devices.length > 0
697
719
  ? h("div", { className: "mc-picker-group" },
698
720
  h("div", { className: "mc-picker-label" }, "Online devices"),
699
- devices.map((d) => h("button", { key: d.serial, className: "mc-picker-row" + (d.serial === serial ? " on" : ""), onClick: () => pick(d.serial) },
721
+ devices.map((d) => h("div", { key: d.serial, className: "mc-picker-row" + (d.serial === serial ? " on" : ""), onClick: () => pick(d.serial) },
700
722
  h("span", null, d.kind === "emulator" ? "🖥" : "📱"),
701
- h("span", { className: "mc-serial" }, d.serial),
723
+ h("span", { className: "mc-serial" }, d.avd ? d.avd + " · " + d.serial : d.serial),
702
724
  d.streaming && h("span", { className: "mc-badge" }, "streaming"),
725
+ d.kind === "emulator" && h("span", { className: "sp" }),
726
+ d.kind === "emulator" && h("button", {
727
+ className: "mc-btn", disabled: booting !== "" || powering !== "",
728
+ title: "Shut down this emulator (adb emu kill)",
729
+ onClick: (e) => { e.stopPropagation(); powerOff(d.serial); },
730
+ }, powering === d.serial ? "stopping…" : "⏻ off"),
703
731
  )),
704
732
  )
705
733
  : h("div", { className: "mc-picker-hint" }, "No device attached"),
706
- avds.length > 0 && h("div", { className: "mc-picker-group" },
734
+ // Running AVDs already have their row (with off) above;
735
+ // the boot list shows only the stopped ones.
736
+ avds.filter((avd) => !runningAvds.has(avd)).length > 0 && h("div", { className: "mc-picker-group" },
707
737
  h("div", { className: "mc-picker-label" }, "AVDs"),
708
- avds.map((avd) => h("div", { key: avd, className: "mc-picker-row" },
738
+ avds.filter((avd) => !runningAvds.has(avd)).map((avd) => h("div", { key: avd, className: "mc-picker-row" },
709
739
  h("span", null, "▫ " + avd),
710
740
  h("span", { className: "sp" }),
711
741
  h("button", {
712
- className: "mc-btn", disabled: booting !== "",
742
+ className: "mc-btn", disabled: booting !== "" || powering !== "",
713
743
  title: "Boot this AVD (runs detached; it appears online when ready)",
714
744
  onClick: () => boot(avd),
715
745
  }, booting === avd ? "booting…" : "⏻ boot"),
@@ -724,12 +754,14 @@ window.__ModuleLoader__.load({
724
754
  ),
725
755
  ),
726
756
  // 0.9.0 co-op: side-by-side second pane (needs two online devices).
727
- devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch two devices side by side", onClick: onToggleCoop }, "⧉"),
757
+ 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 }, "⧉"),
728
758
  ),
729
759
  error !== "" && h("div", { className: "mc-error" }, error),
730
760
  booting !== "" && h("div", { className: "mc-status" },
731
761
  h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
732
- h("div", { className: "mc-toolbar" },
762
+ // 0.11.0 audit: no ghost affordances — the toolbar appears once a
763
+ // device is selected (disabled-grey read as broken).
764
+ serial !== "" && h("div", { className: "mc-toolbar" },
733
765
  toolbar.map((item) => h("button", { key: item.title, title: item.title, disabled: serial === "", onClick: item.run },
734
766
  h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
735
767
  h("path", { d: item.icon }),
@@ -779,7 +811,7 @@ window.__ModuleLoader__.load({
779
811
  /** Co-op second pane (0.9.0): a compact stream view for a second device —
780
812
  * device select, live image, tap/drag. No toolbar/menu/size knobs; the
781
813
  * full card next to it carries those for whichever device it shows. */
782
- function CoopPane({ excludeSerial }) {
814
+ function CoopPane({ excludeSerial, sizeMode, frame }) {
783
815
  const [devices, setDevices] = useState([]);
784
816
  const [serial, setSerial] = useState("");
785
817
  const [error, setError] = useState("");
@@ -811,18 +843,18 @@ window.__ModuleLoader__.load({
811
843
 
812
844
  return h("div", { className: "mc-card mc-live mc-coop-pane" },
813
845
  h("div", { className: "mc-card-head" },
814
- h("span", null, "Player B"),
846
+ h("span", null, "Device 2"),
815
847
  live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
816
848
  h("span", { className: "sp" }),
817
849
  h("select", { className: "mc-input mc-coop-select", value: serial, onChange: (e) => { setSerial(e.target.value); live.reset(); } },
818
850
  devices.length === 0 && h("option", { value: "" }, "no device"),
819
- devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (A)" : d.streaming ? " ●" : ""))),
851
+ devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
820
852
  ),
821
853
  ),
822
854
  error !== "" && h("div", { className: "mc-error" }, error),
823
- h("div", { className: "mc-live-stage" },
855
+ h("div", { className: stageClassFor(frame) },
824
856
  live.streamUrl !== ""
825
- ? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", onError: () => grant(serial) })
857
+ ? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
826
858
  : h("div", { className: "mc-live-off" }, devices.length === 0 ? "No second device — boot another emulator." : "Connecting to " + serial + "..."),
827
859
  ),
828
860
  h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device"),
@@ -834,9 +866,13 @@ window.__ModuleLoader__.load({
834
866
  function LiveStreamSection() {
835
867
  const [coop, setCoop] = useState(false);
836
868
  const [serialA, setSerialA] = useState("");
869
+ // Shared knobs (0.11.0): Device 1's Size/Frame controls drive both
870
+ // panes, so the pair always renders at the same height.
871
+ const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
872
+ const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
837
873
  return h("div", { className: "mc-live-section" + (coop ? " coop" : "") },
838
- h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA }),
839
- coop && h(CoopPane, { excludeSerial: serialA }),
874
+ h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA, sizeMode, setSizeMode, frame, setFrame }),
875
+ coop && h(CoopPane, { excludeSerial: serialA, sizeMode, frame }),
840
876
  );
841
877
  }
842
878
 
@@ -883,7 +919,7 @@ window.__ModuleLoader__.load({
883
919
  ok
884
920
  ? h("span", null, "device_screen reads text from screenshots with local OCR.")
885
921
  : h("span", null, "Without it, device_screen returns the screenshot and UI tree but no OCR text — install to make the plugin work perfectly."),
886
- h("span", { className: "mc-hint" }, "PaddleOCR 2.x · fully local · one-time install (~2 min, needs network once)"),
922
+ h("span", { className: "mc-hint" }, "PaddleOCR 3.x · fully local · one-time install (~2 min, needs network once)"),
887
923
  ),
888
924
  error !== "" && h("div", { className: "mc-error" }, error),
889
925
  (installing || failed) && (status?.log?.length > 0) && h("pre", { className: "mc-logline" }, status.log.join("\n")),
package/lib/index.js CHANGED
@@ -140,6 +140,8 @@ function makeRoutes(engine, config, stream) {
140
140
  const info = await engine.info({ directory })
141
141
  // 0.10.0: Android's device view is the Live device stream, not serve-avd.
142
142
  // The panel's pill shows attached-device count instead of a server state.
143
+ // 0.11.0: os lets the client hide the iOS card on non-mac hosts.
144
+ info.os = process.platform
143
145
  try { info.deviceCount = (await DeviceBuild.devices()).length } catch { /* no adb: pill stays off */ }
144
146
  writeJson(res, 200, info)
145
147
  } catch (error) {
@@ -502,16 +504,19 @@ function makeRoutes(engine, config, stream) {
502
504
  if (!fence(req, res, true) || !isPost(req, res)) return
503
505
  await readBody(req, res)
504
506
  try {
505
- const [devices, avds] = await Promise.all([
507
+ const [rows, avds] = await Promise.all([
506
508
  stream.host.listDevices(),
507
509
  DeviceBuild.androidAvds().catch(() => []),
508
510
  ])
509
511
  const streaming = new Set(stream.host.runningSerials())
510
- writeJson(res, 200, {
511
- ok: true,
512
- avds,
513
- devices: devices.map((device) => ({ ...device, ...(streaming.has(device.serial) ? { streaming: true } : {}) })),
514
- })
512
+ // Attach the AVD name to online emulators so the picker can hide them
513
+ // from the boot list and label its rows (0.11.0).
514
+ const devices = await Promise.all(rows.map(async (device) => ({
515
+ ...device,
516
+ ...(streaming.has(device.serial) ? { streaming: true } : {}),
517
+ ...(device.serial.startsWith('emulator-') ? { avd: await DeviceBuild.avdName(device.serial).catch(() => undefined) } : {}),
518
+ })))
519
+ writeJson(res, 200, { ok: true, avds, devices })
515
520
  } catch (error) {
516
521
  writeJson(res, 503, { code: 'devices_unavailable', error: error instanceof Error ? error.message : String(error) })
517
522
  }
@@ -570,6 +575,30 @@ function makeRoutes(engine, config, stream) {
570
575
  }
571
576
  },
572
577
  },
578
+ // POST /api/dsh-mobilecode/off {serial} — the picker's ⏻ off button (0.11.0):
579
+ // graceful `adb emu kill` for a RUNNING emulator. Physical devices are
580
+ // refused (adb has no power-off verb for phones); starting is /boot's job.
581
+ {
582
+ kind: 'exact',
583
+ path: API_BASE + '/off',
584
+ handler: async (req, res) => {
585
+ if (!fence(req, res, true) || !isPost(req, res)) return
586
+ const body = await readBody(req, res)
587
+ if (body === undefined) return
588
+ const serial = String(body.serial ?? '').trim()
589
+ if (serial === '' || !/^[\w][\w.:-]{0,63}$/.test(serial)) { writeJson(res, 400, { error: 'serial must be an attached device serial' }); return }
590
+ try {
591
+ const rows = await DeviceBuild.devices()
592
+ const row = rows.find((item) => item.serial === serial && item.state === 'device')
593
+ if (!row) { writeJson(res, 404, { error: `${serial} is not online` }); return }
594
+ if (!serial.startsWith('emulator-')) { writeJson(res, 400, { error: 'only emulators can be powered off here — a physical device keeps its own power button' }); return }
595
+ await DeviceBuild.adbRun(serial, ['emu', 'kill'])
596
+ writeJson(res, 200, { ok: true, serial, killed: true })
597
+ } catch (error) {
598
+ writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
599
+ }
600
+ },
601
+ },
573
602
  // POST /api/dsh-mobilecode/stream/still {device?} — capture ONE fresh
574
603
  // screencap PNG and serve it; returns {path, width, height, dataUrl}. The
575
604
  // panel's Screenshot button uses this (a still is the current pixels, unlike
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.10.1",
4
+ "version": "0.11.1",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {