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 +18 -9
- package/lib/client.js +53 -14
- package/lib/index.js +46 -1
- package/package.json +1 -1
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),
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
129
|
-
|
|
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:
|
|
9
|
-
*
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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" }, "
|
|
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
|
|
679
|
-
avds.map((avd) => h("div", { key: avd, className: "mc-picker-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|