dsh-mobilecode 0.11.0 → 0.11.2

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 (3) hide show
  1. package/README.md +14 -10
  2. package/lib/client.js +56 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -133,22 +133,26 @@ card is **Device 1**; co-op docks a second, **Device 2**. It is produced
133
133
  stage, and a ☰ **device menu** adds notifications / quick settings / collapse /
134
134
  lock / wake / assistant.
135
135
  - The header **device picker** groups online devices (🖥 emulator / 📱 physical,
136
- streaming badge) and lists configured AVDs with a one-click **⏻ boot** button
136
+ streaming badge) each online emulator row carries a **⏻ off** power button
137
+ (`POST /off` → `adb emu kill`; physical devices are refused) — and lists
138
+ *stopped* configured AVDs with a one-click **⏻ boot** button
137
139
  (`POST /boot`, the merged device-start action from the retired Start-server
138
140
  flow: validate → spawn detached → adopt if already running → wait boot
139
- complete → the card streams it immediately); a **Live** badge shows the
140
- streamed serial. Quick
141
- sizes (Fit / 100% / S·240 / M·320 presets + a select) and frame styles
142
- (none / bezel / device) reshape the stage.
141
+ complete → the card streams it immediately); a running AVD appears only as
142
+ its online row, never a duplicate boot entry. A **Live** badge shows the
143
+ streamed serial. Quick sizes (Fit / 100% / S·240 / M·320 presets + a select)
144
+ and frame styles (none / bezel / device) reshape the stage — each stream pane
145
+ carries its own copy of these controls (0.11.2).
143
146
  - **Screenshot** captures a still via `POST /stream/still` (a real `screencap`,
144
147
  embedded as a data URL up to 4 MB) and flips the stage to a still view with a
145
148
  "back to Live" link.
146
149
  - **Co-op split view (⧉, v0.9.0)**: with two or more online devices the card
147
- header grows a ⧉ toggle that docks a compact **Player B** pane beside it —
148
- its own device select, its own live `<img>`, and its own tap/drag control, so
149
- a human watches both players fight at the same instant. Each pane grants its
150
- own HMAC capability; closing one pane never stalls or disturbs the other
151
- stream.
150
+ header grows a ⧉ toggle that docks a second **Device 2** pane beside it —
151
+ 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 and share
153
+ one CSS height cap, so the pair starts identical and can be styled
154
+ independently. Each pane grants its own HMAC capability; closing one pane
155
+ never stalls or disturbs the other stream.
152
156
  - **Security**: every stream route sits behind a loopback + trusted-browser
153
157
  transport fence (peer address, loopback `Host`, `Sec-Fetch-Site` / `Origin` —
154
158
  so a LAN client cannot spoof localhost and a DNS-rebinding `Host` is rejected),
package/lib/client.js CHANGED
@@ -118,14 +118,14 @@ window.__ModuleLoader__.load({
118
118
  .mc-live-bar select { background: light-dark(#fff, #20242b); color: inherit; border: 1px solid light-dark(rgba(0,0,0,.18), rgba(255,255,255,.18)); border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 12px; max-width: 200px; }
119
119
  .mc-live-stage { position: relative; align-self: center; background: #000; border-radius: 10px; overflow: hidden; line-height: 0; box-shadow: inset 0 0 0 1px light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); }
120
120
  .mc-live-stage img { display: block; max-width: 100%; max-height: 60vh; width: auto; height: auto; cursor: crosshair; touch-action: none; user-select: none; }
121
- .mc-live-stage .mc-live-off { display: flex; align-items: center; justify-content: center; width: 260px; height: 460px; color: #9aa0a8; font-size: 13px; line-height: 1.5; text-align: center; }
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
124
  .mc-live-section.coop { display: grid; grid-template-columns: repeat(2, minmax(0, 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
- .mc-live-section.coop .mc-live-stage img { max-height: 48vh; }
128
- .mc-live-section.coop .mc-live-off { width: 180px; height: 340px; }
127
+ /* No coop-specific height cap: both panes must share Device 1's 60vh rule so
128
+ they match at every size, Fit included (0.11.2). */
129
129
  .mc-coop-select { width: auto; padding: 3px 6px; font-size: 12px; }
130
130
  .mc-live-nav { display: flex; align-items: center; justify-content: center; gap: 8px; }
131
131
  .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; }
@@ -196,6 +196,42 @@ window.__ModuleLoader__.load({
196
196
  ? { width: m.value + "px", maxWidth: "100%", maxHeight: "60vh" }
197
197
  : {});
198
198
  const stageClassFor = (f) => "mc-live-stage" + (f !== "none" ? " " + f : "");
199
+ const SIZE_PRESETS = [
200
+ { label: "Fit", mode: { mode: "fit" } },
201
+ { label: "100%", mode: { mode: "pct", value: 100 } },
202
+ { label: "S · 240", mode: { mode: "px", value: 240 } },
203
+ { label: "M · 320", mode: { mode: "px", value: 320 } },
204
+ ];
205
+ /** Size presets + px/% select + frame seg — shared by both stream panes
206
+ * (0.11.2) so Device 2 gets the same controls as Device 1. */
207
+ function StageControls({ sizeMode, setSizeMode, frame, setFrame }) {
208
+ const isCurrent = (m) => m.mode === sizeMode.mode && (m.value === undefined || m.value === sizeMode.value);
209
+ return h("div", { className: "mc-live-size" },
210
+ h("span", { className: "mc-hint" }, "Size"),
211
+ h("div", { className: "mc-seg" }, SIZE_PRESETS.map((p) => h("button", { key: p.label, "data-on": isCurrent(p.mode) || undefined, onClick: () => setSizeMode(p.mode) }, p.label))),
212
+ h("select", { className: "mc-input mc-live-sizesel", value: sizeMode.mode === "pct" ? sizeMode.value + "%" : sizeMode.mode === "px" ? String(sizeMode.value) : "fit", onChange: (e) => {
213
+ const v = e.target.value;
214
+ if (v === "fit") setSizeMode({ mode: "fit" });
215
+ else if (v.endsWith("%")) setSizeMode({ mode: "pct", value: Number(v.slice(0, -1)) });
216
+ else setSizeMode({ mode: "px", value: Number(v) });
217
+ } },
218
+ h("option", { value: "fit" }, "fit"),
219
+ h("option", { value: "50%" }, "50%"),
220
+ h("option", { value: "75%" }, "75%"),
221
+ h("option", { value: "100%" }, "100%"),
222
+ h("option", { value: "125%" }, "125%"),
223
+ h("option", { value: "240" }, "S · 240px"),
224
+ h("option", { value: "320" }, "M · 320px"),
225
+ h("option", { value: "420" }, "L · 420px"),
226
+ ),
227
+ h("span", { className: "mc-hint" }, "Frame"),
228
+ h("div", { className: "mc-seg" },
229
+ h("button", { "data-on": frame === "none" || undefined, onClick: () => setFrame("none") }, "none"),
230
+ h("button", { "data-on": frame === "bezel" || undefined, onClick: () => setFrame("bezel") }, "bezel"),
231
+ h("button", { "data-on": frame === "device" || undefined, onClick: () => setFrame("device") }, "device"),
232
+ ),
233
+ );
234
+ }
199
235
 
200
236
  function PanelController() {
201
237
  let panelOpen = false;
@@ -585,7 +621,9 @@ window.__ModuleLoader__.load({
585
621
 
586
622
  /** Device 1 stream: picker-popover header, SVG toolbar pill, device
587
623
  * menu, quick sizes + frame styles, and tap/drag with duration. */
588
- function LiveDeviceCard({ coopOn, onToggleCoop, onSerial, sizeMode, setSizeMode, frame, setFrame }) {
624
+ function LiveDeviceCard({ coopOn, onToggleCoop, onSerial }) {
625
+ const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
626
+ const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
589
627
  const [devices, setDevices] = useState([]);
590
628
  const [avds, setAvds] = useState([]);
591
629
  const [serial, setSerial] = useState("");
@@ -699,13 +737,6 @@ window.__ModuleLoader__.load({
699
737
  { action: "wake", label: "Wake" },
700
738
  { action: "assistant", label: "Assistant" },
701
739
  ];
702
- const SIZE_PRESETS = [
703
- { label: "Fit", mode: { mode: "fit" } },
704
- { label: "100%", mode: { mode: "pct", value: 100 } },
705
- { label: "S · 240", mode: { mode: "px", value: 240 } },
706
- { label: "M · 320", mode: { mode: "px", value: 320 } },
707
- ];
708
- const isCurrent = (m) => m.mode === sizeMode.mode && (m.value === undefined || m.value === sizeMode.value);
709
740
 
710
741
  return h("div", { className: "mc-card mc-live" },
711
742
  h("div", { className: "mc-card-head" },
@@ -781,39 +812,19 @@ window.__ModuleLoader__.load({
781
812
  ? h("div", { className: "mc-live-cap" }, "Still captured" + (still?.width ? " (" + still.width + "x" + still.height + ")" : "") + " · ",
782
813
  h("a", { className: "mc-log-toggle", onClick: showLive }, "back to Live"))
783
814
  : h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive the device"),
784
- h("div", { className: "mc-live-size" },
785
- h("span", { className: "mc-hint" }, "Size"),
786
- h("div", { className: "mc-seg" }, SIZE_PRESETS.map((p) => h("button", { key: p.label, "data-on": isCurrent(p.mode) || undefined, onClick: () => setSizeMode(p.mode) }, p.label))),
787
- h("select", { className: "mc-input mc-live-sizesel", value: sizeMode.mode === "pct" ? sizeMode.value + "%" : sizeMode.mode === "px" ? String(sizeMode.value) : "fit", onChange: (e) => {
788
- const v = e.target.value;
789
- if (v === "fit") setSizeMode({ mode: "fit" });
790
- else if (v.endsWith("%")) setSizeMode({ mode: "pct", value: Number(v.slice(0, -1)) });
791
- else setSizeMode({ mode: "px", value: Number(v) });
792
- } },
793
- h("option", { value: "fit" }, "fit"),
794
- h("option", { value: "50%" }, "50%"),
795
- h("option", { value: "75%" }, "75%"),
796
- h("option", { value: "100%" }, "100%"),
797
- h("option", { value: "125%" }, "125%"),
798
- h("option", { value: "240" }, "S · 240px"),
799
- h("option", { value: "320" }, "M · 320px"),
800
- h("option", { value: "420" }, "L · 420px"),
801
- ),
802
- h("span", { className: "mc-hint" }, "Frame"),
803
- h("div", { className: "mc-seg" },
804
- h("button", { "data-on": frame === "none" || undefined, onClick: () => setFrame("none") }, "none"),
805
- h("button", { "data-on": frame === "bezel" || undefined, onClick: () => setFrame("bezel") }, "bezel"),
806
- h("button", { "data-on": frame === "device" || undefined, onClick: () => setFrame("device") }, "device"),
807
- ),
808
- ),
815
+ h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
809
816
  );
810
817
  }
811
818
  /** Co-op second pane (0.9.0): a compact stream view for a second device —
812
- * device select, live image, tap/drag. No toolbar/menu/size knobs; the
813
- * full card next to it carries those for whichever device it shows. */
814
- function CoopPane({ excludeSerial, sizeMode, frame }) {
819
+ * device select, live image, tap/drag — and (0.11.2) its own Size/Frame
820
+ * row, mirroring Device 1's controls. No toolbar/menu: those belong to
821
+ * the full card. Defaults are identical, so the panes match out of the
822
+ * box and can be adjusted independently. */
823
+ function CoopPane({ excludeSerial }) {
815
824
  const [devices, setDevices] = useState([]);
816
825
  const [serial, setSerial] = useState("");
826
+ const [sizeMode, setSizeMode] = useState({ mode: "fit" });
827
+ const [frame, setFrame] = useState("none");
817
828
  const [error, setError] = useState("");
818
829
  const live = useLiveStream({ serial, onError: setError });
819
830
  const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
@@ -858,21 +869,19 @@ window.__ModuleLoader__.load({
858
869
  : h("div", { className: "mc-live-off" }, devices.length === 0 ? "No second device — boot another emulator." : "Connecting to " + serial + "..."),
859
870
  ),
860
871
  h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device"),
872
+ h(StageControls, { sizeMode, setSizeMode, frame, setFrame }),
861
873
  );
862
874
  }
863
875
 
864
876
  /** Stream section: the classic single card, plus a co-op mode that docks
865
- * a second pane next to it for two-player testing. */
877
+ * a second pane next to it for two-player testing. Each pane owns its
878
+ * Size/Frame knobs (0.11.2); the shared CSS base makes them match. */
866
879
  function LiveStreamSection() {
867
880
  const [coop, setCoop] = useState(false);
868
881
  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'
873
882
  return h("div", { className: "mc-live-section" + (coop ? " coop" : "") },
874
- h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA, sizeMode, setSizeMode, frame, setFrame }),
875
- coop && h(CoopPane, { excludeSerial: serialA, sizeMode, frame }),
883
+ h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA }),
884
+ coop && h(CoopPane, { excludeSerial: serialA }),
876
885
  );
877
886
  }
878
887
 
@@ -919,7 +928,7 @@ window.__ModuleLoader__.load({
919
928
  ok
920
929
  ? h("span", null, "device_screen reads text from screenshots with local OCR.")
921
930
  : h("span", null, "Without it, device_screen returns the screenshot and UI tree but no OCR text — install to make the plugin work perfectly."),
922
- h("span", { className: "mc-hint" }, "PaddleOCR 2.x · fully local · one-time install (~2 min, needs network once)"),
931
+ h("span", { className: "mc-hint" }, "PaddleOCR 3.x · fully local · one-time install (~2 min, needs network once)"),
923
932
  ),
924
933
  error !== "" && h("div", { className: "mc-error" }, error),
925
934
  (installing || failed) && (status?.log?.length > 0) && h("pre", { className: "mc-logline" }, status.log.join("\n")),
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.0",
4
+ "version": "0.11.2",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {