dsh-mobilecode 0.9.1 → 0.10.0

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
@@ -6,9 +6,11 @@ with embedded iOS Simulator / Android Emulator support) into
6
6
  [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness).
7
7
 
8
8
  It detects the mobile project in a directory (iOS / Android, Expo / React
9
- Native / native), starts the `serve-sim` / `serve-avd` preview servers, builds,
10
- installs and launches the app on the booted simulator or emulator and exposes
11
- the same to the agent through tools.
9
+ Native / native), builds, installs and launches the app on the booted
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.)
12
14
 
13
15
  ## Installation
14
16
 
@@ -30,10 +32,12 @@ If you find this useful, you can support development at [ko-fi.com/spix18](https
30
32
  drawer:
31
33
 
32
34
  - project directory input (persisted in localStorage) + Detect
33
- - platform pills (iOS / Android) with live server status
34
- - per platform: **Start/Stop server**, **Run app / Stop app**, an embedded
35
- iframe of the serve-sim / serve-avd stream, build status/step/error and an
36
- expandable log tail
35
+ - platform pills Android: attached-device status (its view is the Live
36
+ device card); iOS: preview-server status
37
+ - per platform: **Run app / Stop app**, build status/step/error and an
38
+ expandable log tail. iOS additionally **Start/Stop server** with the
39
+ embedded serve-sim iframe — Android's duplicated serve-avd preview was
40
+ merged into the Live device card in 0.10.0
37
41
  - a Metro (bundler) status card
38
42
  - polls `GET /api/dsh-mobilecode` every 2 s while open
39
43
 
@@ -125,8 +129,11 @@ The Devices pane shows a real-time mirror of the attached device. It is produced
125
129
  stage, and a ☰ **device menu** adds notifications / quick settings / collapse /
126
130
  lock / wake / assistant.
127
131
  - The header **device picker** groups online devices (🖥 emulator / 📱 physical,
128
- streaming badge) and lists configured AVDs as boot hints (`device_boot` starts
129
- one the picker never does); a **Live** badge shows the streamed serial. Quick
132
+ streaming badge) and lists configured AVDs with a one-click **⏻ boot** button
133
+ (`POST /boot`, the merged device-start action from the retired Start-server
134
+ flow: validate → spawn detached → adopt if already running → wait boot
135
+ complete → the card streams it immediately); a **Live** badge shows the
136
+ streamed serial. Quick
130
137
  sizes (Fit / 100% / S·240 / M·320 presets + a select) and frame styles
131
138
  (none / bezel / device) reshape the stage.
132
139
  - **Screenshot** captures a still via `POST /stream/still` (a real `screencap`,
@@ -318,6 +325,8 @@ group — plus setup endpoints: `GET /welcome`, `POST /welcome/dismiss`,
318
325
  plus the live-stream routes `GET /stream/status`, `POST /stream/grant`,
319
326
  `POST /stream/control` (tap / swipe / key / long-press, coalesced),
320
327
  `POST /stream/devices` (online devices **and** configured AVDs),
328
+ `POST /boot {avd}` (the panel's merged one-click AVD boot — validated name,
329
+ adopt-if-running, detached spawn, resolves once boot-completed),
321
330
  `POST /stream/still` (one `screencap` as a data URL up to 4 MB),
322
331
  `POST /stream/device-action` (the `device_action` verbs over the panel fence)
323
332
  and `GET /stream/{token}` (the multipart frame body) — plus the co-op mesh
package/lib/client.js CHANGED
@@ -5,8 +5,10 @@
5
5
  * device-preview UI:
6
6
  * - sidebar entry row toggling the pane (DOM-level injection, self-healing),
7
7
  * - directory input (remembers the last one) + platform pills from detect,
8
- * - per platform: start/stop the preview server, run/stop the app,
9
- * - embedded iframe of the serve-sim / serve-avd stream,
8
+ * - per platform: run/stop the app + build status (iOS only: start/stop
9
+ * the serve-sim preview server with its embedded iframe),
10
+ * - Live device card: the in-process stream — 0.10.0 merged the Android
11
+ * "Start server" into it (serve-avd retired; picker ⏻ boot → POST /boot),
10
12
  * - build status, error and expandable log tail,
11
13
  * - polls GET /api/dsh-mobilecode every ~2s while open.
12
14
  *
@@ -158,6 +160,8 @@ window.__ModuleLoader__.load({
158
160
  .mc-picker-row:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
159
161
  .mc-picker-row.on { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); }
160
162
  .mc-picker-hint { font-size: 11px; color: light-dark(#6b7078, #9aa0a8); padding: 4px 8px; }
163
+ .mc-picker-row .sp { flex: 1; }
164
+ .mc-picker-row .mc-btn { padding: 2px 8px; font-size: 11px; }
161
165
  .mc-menu-pop { position: absolute; top: calc(100% + 4px); right: 0; z-index: 50; min-width: 180px; background: light-dark(#fff, #20242b); color: inherit; border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 10px; box-shadow: 0 8px 30px rgba(0,0,0,.35); padding: 4px; display: flex; flex-direction: column; }
162
166
  .mc-menu-pop button { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border: 0; border-radius: 7px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer; text-align: left; }
163
167
  .mc-menu-pop button:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
@@ -333,8 +337,13 @@ window.__ModuleLoader__.load({
333
337
  error !== "" && h("div", { className: "mc-error" }, error),
334
338
  h("div", { className: "mc-pills" },
335
339
  platforms.map((platform) => {
336
- const running = server(platform)?.status === "running";
337
- return h("span", { key: platform, className: "mc-pill", "data-on": running ? "true" : undefined },
340
+ // 0.10.0: Android's "server" IS the Live device stream — the pill
341
+ // tracks attached devices, not a serve-avd process.
342
+ const running = platform === "android"
343
+ ? (info?.deviceCount ?? 0) > 0
344
+ : server(platform)?.status === "running";
345
+ 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" },
338
347
  h("span", { className: "mc-dot " + (running ? "on" : "off") }),
339
348
  label(platform),
340
349
  );
@@ -344,17 +353,21 @@ window.__ModuleLoader__.load({
344
353
  platforms.map((platform) => {
345
354
  const srv = server(platform);
346
355
  const bld = build(platform);
347
- const busyHere = busy !== "" && busy.startsWith(platform);
356
+ // 0.10.0 merge: "Start server" (serve-avd iframe) and "Live device" were
357
+ // two views of the same screen. The in-process stream won; the Android
358
+ // card keeps project controls only. iOS keeps serve-sim (its only view).
359
+ const preview = platform === "ios";
348
360
  return h("div", { key: platform, className: "mc-card" },
349
361
  h("div", { className: "mc-card-head" },
350
362
  h("span", null, label(platform)),
351
363
  h("span", { className: "sp" }),
352
- srv
364
+ preview && (srv
353
365
  ? h("button", { className: "mc-btn", disabled: busy !== "", onClick: () => act(platform + "-stop-server", "/stop", { platform }) },
354
366
  srv.status === "running" ? "Stop server" : "Server " + srv.status)
355
- : h("button", { className: "mc-btn primary", disabled: busy !== "", onClick: () => act(platform + "-start-server", "/start", { platform }) }, "Start server"),
367
+ : 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 ↑"),
356
369
  ),
357
- srv?.url
370
+ preview && (srv?.url
358
371
  ? h("iframe", {
359
372
  key: frameKey,
360
373
  className: "mc-frame",
@@ -364,7 +377,7 @@ window.__ModuleLoader__.load({
364
377
  })
365
378
  : srv?.status === "starting"
366
379
  ? h("div", { className: "mc-status" }, h("span", null, h("span", { className: "mc-dot warn" }), " Starting preview server…"))
367
- : null,
380
+ : null),
368
381
  h("div", { className: "mc-row" },
369
382
  h("button", {
370
383
  className: "mc-btn primary",
@@ -404,8 +417,8 @@ window.__ModuleLoader__.load({
404
417
  openLogs["bundler"] ? "Hide log" : "Show log (" + info.bundler.log.length + ")"),
405
418
  openLogs["bundler"] && h("pre", { className: "mc-log" }, info.bundler.log.join("\n")),
406
419
  ),
407
- info?.servers?.length === 0 && info?.builds?.length === 0 &&
408
- h("div", { className: "mc-empty" }, "No preview server running. Pick a project directory and press Run app."),
420
+ 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."),
409
422
  ),
410
423
  h("div", { className: "mc-footer" },
411
424
  h("span", null, "dsh-mobilecode · polls every 2s"),
@@ -574,6 +587,7 @@ window.__ModuleLoader__.load({
574
587
  const [view, setView] = useState("live"); // 'live' | 'still'
575
588
  const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
576
589
  const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
590
+ const [booting, setBooting] = useState(""); // AVD name while POST /boot is in flight
577
591
  const live = useLiveStream({ serial, onError: setError });
578
592
  const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
579
593
  const control = live.control;
@@ -624,6 +638,21 @@ window.__ModuleLoader__.load({
624
638
  };
625
639
 
626
640
  const pick = (device) => { setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
641
+ // 0.10.0 merged "Start server": boot a configured AVD from the picker.
642
+ // The route waits for boot completion; then we re-list and stream it.
643
+ const boot = async (avd) => {
644
+ setBooting(avd); setError("");
645
+ try {
646
+ const r = await streamPost("/boot", { avd });
647
+ await refreshDevices();
648
+ if (r.serial) pick(r.serial);
649
+ } catch (err) {
650
+ setError(err instanceof Error ? err.message : String(err));
651
+ await refreshDevices();
652
+ } finally {
653
+ setBooting("");
654
+ }
655
+ };
627
656
  const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
628
657
 
629
658
  const sizeStyle = sizeMode.mode === "pct"
@@ -675,8 +704,16 @@ window.__ModuleLoader__.load({
675
704
  )
676
705
  : h("div", { className: "mc-picker-hint" }, "No device attached"),
677
706
  avds.length > 0 && h("div", { className: "mc-picker-group" },
678
- h("div", { className: "mc-picker-label" }, "AVDs — start with device_boot"),
679
- avds.map((avd) => h("div", { key: avd, className: "mc-picker-hint" }, "▫ " + avd)),
707
+ h("div", { className: "mc-picker-label" }, "AVDs"),
708
+ avds.map((avd) => h("div", { key: avd, className: "mc-picker-row" },
709
+ h("span", null, "▫ " + avd),
710
+ h("span", { className: "sp" }),
711
+ h("button", {
712
+ className: "mc-btn", disabled: booting !== "",
713
+ title: "Boot this AVD (runs detached; it appears online when ready)",
714
+ onClick: () => boot(avd),
715
+ }, booting === avd ? "booting…" : "⏻ boot"),
716
+ )),
680
717
  ),
681
718
  ),
682
719
  ),
@@ -690,6 +727,8 @@ window.__ModuleLoader__.load({
690
727
  devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch two devices side by side", onClick: onToggleCoop }, "⧉"),
691
728
  ),
692
729
  error !== "" && h("div", { className: "mc-error" }, error),
730
+ booting !== "" && h("div", { className: "mc-status" },
731
+ h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
693
732
  h("div", { className: "mc-toolbar" },
694
733
  toolbar.map((item) => h("button", { key: item.title, title: item.title, disabled: serial === "", onClick: item.run },
695
734
  h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
@@ -703,7 +742,7 @@ window.__ModuleLoader__.load({
703
742
  : live.streamUrl !== ""
704
743
  ? h("img", { ...live.imgProps, src: live.streamUrl, alt: "device screen", style: sizeStyle, onError: () => grant(serial) })
705
744
  : h("div", { className: "mc-live-off" }, devices.length === 0
706
- ? "No device attached. Boot one with device_boot, or press Run app."
745
+ ? "No device attached boot an AVD from the ▾ picker, or press Run app."
707
746
  : "Connecting to " + serial + "..."),
708
747
  ),
709
748
  view === "still"
package/lib/index.js CHANGED
@@ -137,7 +137,11 @@ function makeRoutes(engine, config, stream) {
137
137
  const url = new URL(req.url ?? '/', 'http://localhost')
138
138
  const directory = url.searchParams.get('directory') ?? resolveDirectory({}, config)
139
139
  try {
140
- writeJson(res, 200, await engine.info({ directory }))
140
+ const info = await engine.info({ directory })
141
+ // 0.10.0: Android's device view is the Live device stream, not serve-avd.
142
+ // The panel's pill shows attached-device count instead of a server state.
143
+ try { info.deviceCount = (await DeviceBuild.devices()).length } catch { /* no adb: pill stays off */ }
144
+ writeJson(res, 200, info)
141
145
  } catch (error) {
142
146
  writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
143
147
  }
@@ -513,6 +517,47 @@ function makeRoutes(engine, config, stream) {
513
517
  }
514
518
  },
515
519
  },
520
+ // POST /api/dsh-mobilecode/boot {avd} — the one-click device boot the
521
+ // merged Live-device section uses (0.10.0). The emulator is a detached
522
+ // daemon (same as device_boot); the response returns once it is online
523
+ // and boot-completed, so the panel can stream it immediately. An already
524
+ // running AVD is adopted without waiting.
525
+ {
526
+ kind: 'exact',
527
+ path: API_BASE + '/boot',
528
+ handler: async (req, res) => {
529
+ if (!fence(req, res, true) || !isPost(req, res)) return
530
+ const body = await readBody(req, res)
531
+ if (body === undefined) return
532
+ const avd = String(body.avd ?? '').trim()
533
+ if (avd === '' || !/^[\w.-]+$/.test(avd)) { writeJson(res, 400, { error: 'avd must be a configured AVD name' }); return }
534
+ try {
535
+ const known = await DeviceBuild.androidAvds()
536
+ if (!known.includes(avd)) { writeJson(res, 400, { error: `no such AVD "${avd}" (have: ${known.join(', ') || 'none'})` }); return }
537
+ // Already running: adopt it instead of starting a duplicate.
538
+ const rows = await DeviceBuild.devices()
539
+ for (const row of rows.filter((item) => item.state === 'device' && item.serial.startsWith('emulator-'))) {
540
+ const name = await DeviceBuild.avdName(row.serial).catch(() => undefined)
541
+ if (name === avd) { writeJson(res, 200, { ok: true, alreadyRunning: true, serial: row.serial, avd }); return }
542
+ }
543
+ if (!DeviceBuild.bootEmulator(avd)) { writeJson(res, 500, { error: `could not launch the emulator for "${avd}"` }); return }
544
+ const deadline = Date.now() + 60_000
545
+ const before = new Set(rows.map((item) => item.serial))
546
+ let serial
547
+ while (Date.now() < deadline) {
548
+ const now = await DeviceBuild.devices()
549
+ const fresh = now.find((item) => item.serial.startsWith('emulator-') && !before.has(item.serial))
550
+ if (fresh) { serial = fresh.serial; break }
551
+ await new Promise((resolve) => setTimeout(resolve, 1000))
552
+ }
553
+ if (!serial) { writeJson(res, 504, { error: `emulator for "${avd}" did not come online within 60s` }); return }
554
+ const booted = await DeviceBuild.waitForBoot(serial, 240_000)
555
+ writeJson(res, booted ? 200 : 504, { ok: booted, serial, avd, booted })
556
+ } catch (error) {
557
+ writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
558
+ }
559
+ },
560
+ },
516
561
  // POST /api/dsh-mobilecode/stream/still {device?} — capture ONE fresh
517
562
  // screencap PNG and serve it; returns {path, width, height, dataUrl}. The
518
563
  // 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.9.1",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {