dsh-mobilecode 0.9.0 → 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
  *
@@ -117,11 +119,13 @@ window.__ModuleLoader__.load({
117
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)); }
118
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; }
119
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; }
120
- /* 0.9.0 co-op: card + second pane share the panel width side by side. */
122
+ /* 0.9.1 co-op: two forced columns side-by-side MUST survive a narrow drawer. */
121
123
  .mc-live-section { display: flex; flex-direction: column; gap: 10px; }
122
- .mc-live-section.coop { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
123
- .mc-live-section.coop > .mc-card { flex: 1 1 320px; min-width: 300px; }
124
+ .mc-live-section.coop { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
125
+ .mc-live-section.coop > .mc-card { min-width: 0; }
126
+ .mc-live-section.coop .mc-card-head { flex-wrap: wrap; }
124
127
  .mc-live-section.coop .mc-live-stage img { max-height: 48vh; }
128
+ .mc-live-section.coop .mc-live-off { width: 180px; height: 340px; }
125
129
  .mc-coop-select { width: auto; padding: 3px 6px; font-size: 12px; }
126
130
  .mc-live-nav { display: flex; align-items: center; justify-content: center; gap: 8px; }
127
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; }
@@ -156,6 +160,8 @@ window.__ModuleLoader__.load({
156
160
  .mc-picker-row:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
157
161
  .mc-picker-row.on { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); }
158
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; }
159
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; }
160
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; }
161
167
  .mc-menu-pop button:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
@@ -331,8 +337,13 @@ window.__ModuleLoader__.load({
331
337
  error !== "" && h("div", { className: "mc-error" }, error),
332
338
  h("div", { className: "mc-pills" },
333
339
  platforms.map((platform) => {
334
- const running = server(platform)?.status === "running";
335
- 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" },
336
347
  h("span", { className: "mc-dot " + (running ? "on" : "off") }),
337
348
  label(platform),
338
349
  );
@@ -342,17 +353,21 @@ window.__ModuleLoader__.load({
342
353
  platforms.map((platform) => {
343
354
  const srv = server(platform);
344
355
  const bld = build(platform);
345
- 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";
346
360
  return h("div", { key: platform, className: "mc-card" },
347
361
  h("div", { className: "mc-card-head" },
348
362
  h("span", null, label(platform)),
349
363
  h("span", { className: "sp" }),
350
- srv
364
+ preview && (srv
351
365
  ? h("button", { className: "mc-btn", disabled: busy !== "", onClick: () => act(platform + "-stop-server", "/stop", { platform }) },
352
366
  srv.status === "running" ? "Stop server" : "Server " + srv.status)
353
- : 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 ↑"),
354
369
  ),
355
- srv?.url
370
+ preview && (srv?.url
356
371
  ? h("iframe", {
357
372
  key: frameKey,
358
373
  className: "mc-frame",
@@ -362,7 +377,7 @@ window.__ModuleLoader__.load({
362
377
  })
363
378
  : srv?.status === "starting"
364
379
  ? h("div", { className: "mc-status" }, h("span", null, h("span", { className: "mc-dot warn" }), " Starting preview server…"))
365
- : null,
380
+ : null),
366
381
  h("div", { className: "mc-row" },
367
382
  h("button", {
368
383
  className: "mc-btn primary",
@@ -402,8 +417,8 @@ window.__ModuleLoader__.load({
402
417
  openLogs["bundler"] ? "Hide log" : "Show log (" + info.bundler.log.length + ")"),
403
418
  openLogs["bundler"] && h("pre", { className: "mc-log" }, info.bundler.log.join("\n")),
404
419
  ),
405
- info?.servers?.length === 0 && info?.builds?.length === 0 &&
406
- 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."),
407
422
  ),
408
423
  h("div", { className: "mc-footer" },
409
424
  h("span", null, "dsh-mobilecode · polls every 2s"),
@@ -572,6 +587,7 @@ window.__ModuleLoader__.load({
572
587
  const [view, setView] = useState("live"); // 'live' | 'still'
573
588
  const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
574
589
  const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
590
+ const [booting, setBooting] = useState(""); // AVD name while POST /boot is in flight
575
591
  const live = useLiveStream({ serial, onError: setError });
576
592
  const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
577
593
  const control = live.control;
@@ -622,6 +638,21 @@ window.__ModuleLoader__.load({
622
638
  };
623
639
 
624
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
+ };
625
656
  const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
626
657
 
627
658
  const sizeStyle = sizeMode.mode === "pct"
@@ -673,8 +704,16 @@ window.__ModuleLoader__.load({
673
704
  )
674
705
  : h("div", { className: "mc-picker-hint" }, "No device attached"),
675
706
  avds.length > 0 && h("div", { className: "mc-picker-group" },
676
- h("div", { className: "mc-picker-label" }, "AVDs — start with device_boot"),
677
- 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
+ )),
678
717
  ),
679
718
  ),
680
719
  ),
@@ -688,6 +727,8 @@ window.__ModuleLoader__.load({
688
727
  devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch two devices side by side", onClick: onToggleCoop }, "⧉"),
689
728
  ),
690
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).")),
691
732
  h("div", { className: "mc-toolbar" },
692
733
  toolbar.map((item) => h("button", { key: item.title, title: item.title, disabled: serial === "", onClick: item.run },
693
734
  h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
@@ -701,7 +742,7 @@ window.__ModuleLoader__.load({
701
742
  : live.streamUrl !== ""
702
743
  ? h("img", { ...live.imgProps, src: live.streamUrl, alt: "device screen", style: sizeStyle, onError: () => grant(serial) })
703
744
  : h("div", { className: "mc-live-off" }, devices.length === 0
704
- ? "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."
705
746
  : "Connecting to " + serial + "..."),
706
747
  ),
707
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.0",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {