vibeview 0.34.0 → 0.36.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/dist/cli.js CHANGED
@@ -23,8 +23,9 @@ import {
23
23
  readDevState,
24
24
  readDevStateFile,
25
25
  readMetroState,
26
+ recordDevUse,
26
27
  runAgentVerb
27
- } from "./chunk-TPZG7NCU.js";
28
+ } from "./chunk-VFCG4U7V.js";
28
29
  import {
29
30
  ApiClient,
30
31
  ApiStatusError,
@@ -45,7 +46,7 @@ import {
45
46
  resolveAuth,
46
47
  saveConfig,
47
48
  uploadApp
48
- } from "./chunk-4D2E2PE4.js";
49
+ } from "./chunk-QRJAFBH3.js";
49
50
 
50
51
  // src/cli.ts
51
52
  import { createRequire } from "module";
@@ -412,6 +413,7 @@ function printStepLines(stepNum, total, status, text, actions, error) {
412
413
  process.stderr.write(` ${icon} Step ${stepNum}/${total}: ${text}
413
414
  `);
414
415
  for (const action of actions ?? []) {
416
+ if (action.tool === "fast_replay") continue;
415
417
  process.stderr.write(` ${chalk2.dim(formatAction(action))}
416
418
  `);
417
419
  }
@@ -1786,6 +1788,7 @@ async function devReload(opts) {
1786
1788
  } catch (err) {
1787
1789
  fail(opts, "reload_failed", err instanceof Error ? err.message : String(err), 1);
1788
1790
  }
1791
+ recordDevUse(state.session_id);
1789
1792
  if (opts.json) {
1790
1793
  console.log(
1791
1794
  JSON.stringify({
@@ -3834,7 +3837,7 @@ function resolveMetadata(opts) {
3834
3837
  }
3835
3838
  var program = new Command();
3836
3839
  program.name("vibeview").description(
3837
- "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop (dev), device control for you or your coding agent (ui-tree, tap, mcp), and tests from CI (run-test, run-suites)"
3840
+ "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop (dev), device control for you or your coding agent (ui-tree, tap, mcp), and tests from CI (run-test, run-suites)"
3838
3841
  ).version(pkg.version);
3839
3842
  program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
3840
3843
  "--device-category <category>",
@@ -4144,7 +4147,7 @@ program.command("dev-reload").description(
4144
4147
  program.command("mcp").description(
4145
4148
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4146
4149
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4147
- const { runMcpServer } = await import("./server-RDGEVJD4.js");
4150
+ const { runMcpServer } = await import("./server-2CDIRTBV.js");
4148
4151
  await runMcpServer({ ...opts, version: pkg.version });
4149
4152
  });
4150
4153
  program.command("agent-setup").description(
@@ -9,6 +9,7 @@ import {
9
9
  buildToolCall,
10
10
  checkDevBuild,
11
11
  checkMetro,
12
+ countsAsUse,
12
13
  createRokuPush,
13
14
  decodeScreenshotResult,
14
15
  describeDevBuild,
@@ -20,17 +21,19 @@ import {
20
21
  loadDevConfig,
21
22
  loadLocalDevConfig,
22
23
  parseBuildId,
24
+ parseIdleWarningSeconds,
23
25
  platformWithArticle,
24
26
  readDevState,
27
+ recordDevUse,
25
28
  renderEnvelope,
26
29
  resolveMetro,
27
30
  startDevSession
28
- } from "./chunk-TPZG7NCU.js";
31
+ } from "./chunk-VFCG4U7V.js";
29
32
  import {
30
33
  ApiClient,
31
34
  NotFoundError,
32
35
  resolveAuth
33
- } from "./chunk-4D2E2PE4.js";
36
+ } from "./chunk-QRJAFBH3.js";
34
37
 
35
38
  // src/mcp/server.ts
36
39
  import { writeFileSync } from "fs";
@@ -241,9 +244,15 @@ function capitalize(text) {
241
244
  return text.charAt(0).toUpperCase() + text.slice(1);
242
245
  }
243
246
  function endedLine(ended) {
247
+ if (ended.status === "replaced") {
248
+ return `session ${ended.sessionId} was taken over by another VibeView dev connection and is no longer held here (it keeps running there)`;
249
+ }
244
250
  const why = ended.reason ? `: ${ended.reason}` : "";
245
251
  return `session ${ended.sessionId} has ended (${ended.status}${why})`;
246
252
  }
253
+ function idleNoticeText(sessionId, seconds) {
254
+ return `Session ${sessionId} ends in ${seconds} s for inactivity: a device action (tap, type, swipe, press, \u2026) keeps it; ui_tree, screenshot, logs, find and wait do not.`;
255
+ }
247
256
  async function explainInactive(sessionId, deps) {
248
257
  let info;
249
258
  try {
@@ -266,7 +275,7 @@ async function explainInactive(sessionId, deps) {
266
275
  function findCommand(toolName) {
267
276
  return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
268
277
  }
269
- async function handleToolCall(toolName, args, deps, extra = {}) {
278
+ async function dispatchToolCall(toolName, args, deps, extra = {}) {
270
279
  if (toolName === "dev_start") {
271
280
  return handleDevStart(args, deps, extra);
272
281
  }
@@ -331,6 +340,55 @@ async function handleToolCall(toolName, args, deps, extra = {}) {
331
340
  }
332
341
  return textResult(renderEnvelope(envelope, toolNameFor(cmd.verb)), envelope, !envelope.ok);
333
342
  }
343
+ async function handleToolCall(toolName, args, deps, extra = {}) {
344
+ const held = deps.devSession.current?.()?.sessionId;
345
+ const dispatched = typeof args.session_id === "string" ? args.session_id.trim() : deps.resolveSessionId();
346
+ const usesBefore = deps.devSession.useGeneration?.();
347
+ const result = await dispatchToolCall(toolName, args, deps, extra);
348
+ noteDeviceUse(toolName, result, deps, held, dispatched);
349
+ const usedSince = usesBefore !== void 0 && (deps.devSession.useGeneration?.() ?? usesBefore) > usesBefore;
350
+ return withIdleNotice(toolName, args, result, deps, held, dispatched, usedSince);
351
+ }
352
+ function noteDeviceUse(toolName, result, deps, held, dispatched) {
353
+ const cmd = findCommand(toolName);
354
+ if (!cmd || !dispatched || !result.structuredContent) return;
355
+ if (!countsAsUse(cmd.tool)) return;
356
+ if (dispatched === held) deps.devSession.noteUse?.(dispatched);
357
+ else recordDevUse(dispatched);
358
+ }
359
+ var IDLE_NOTICE_DISCARD = /* @__PURE__ */ new Set(["dev_start", "dev_stop", "dev_reload"]);
360
+ function withIdleNotice(toolName, args, result, deps, held, dispatched, usedSince = false) {
361
+ const dev = deps.devSession;
362
+ const stopsHeld = toolName === "stop_session" && held !== void 0 && (typeof args.session_id === "string" ? args.session_id.trim() : held) === held;
363
+ if ((IDLE_NOTICE_DISCARD.has(toolName) || stopsHeld) && !result.isError) {
364
+ const about = toolName === "dev_start" ? dev.current?.()?.sessionId : held;
365
+ if (about !== void 0) dev.takeIdleWarning?.(about);
366
+ return result;
367
+ }
368
+ if (findCommand(toolName) && result.structuredContent && typeof result.structuredContent === "object") {
369
+ const own = result.structuredContent.idle_warning;
370
+ if (own !== void 0) {
371
+ const target = dispatched;
372
+ if (target !== void 0 && target === held) dev.takeIdleWarning?.(target);
373
+ const seconds = parseIdleWarningSeconds(own?.seconds_remaining);
374
+ if (usedSince && target === held) return result;
375
+ if (target && seconds !== null) return appendNotice(result, idleNoticeText(target, seconds));
376
+ return result;
377
+ }
378
+ }
379
+ const notice = dev.takeIdleWarning?.() ?? null;
380
+ return notice ? appendNotice(result, notice) : result;
381
+ }
382
+ function appendNotice(result, notice) {
383
+ const i = result.content.findIndex((b) => b.type === "text");
384
+ if (i === -1) return { ...result, content: [...result.content, { type: "text", text: notice }] };
385
+ const content = result.content.slice();
386
+ const block = content[i];
387
+ content[i] = { type: "text", text: `${block.text}
388
+
389
+ ${notice}` };
390
+ return { ...result, content };
391
+ }
334
392
  function screenshotToolResult(envelope, args) {
335
393
  const { image, preview } = splitScreenshotResult(envelope.result);
336
394
  let imageBytes;
@@ -613,7 +671,7 @@ async function handleDevStop(deps) {
613
671
  }
614
672
  if (!current) {
615
673
  return textResult(
616
- ended ? `no dev session was running \u2014 it had already ended. ${capitalize(endedLine(ended))}` : "no dev session was running",
674
+ ended ? ended.status === "replaced" ? `no dev session was running here. ${capitalize(endedLine(ended))}` : `no dev session was running \u2014 it had already ended. ${capitalize(endedLine(ended))}` : "no dev session was running",
617
675
  { session_id: null, stopped: false }
618
676
  );
619
677
  }
@@ -629,7 +687,7 @@ async function handleUploadApp(args, deps) {
629
687
  }
630
688
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
631
689
  const upload = deps.uploadApp ?? (async (f, n) => {
632
- const { uploadAppBuild } = await import("./upload-app-AKD73742.js");
690
+ const { uploadAppBuild } = await import("./upload-app-BPTGBDQB.js");
633
691
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
634
692
  return result2;
635
693
  });
@@ -725,6 +783,7 @@ function createDevSessionController(auth, webUrl) {
725
783
  let ended = null;
726
784
  let reloading = null;
727
785
  let pending = null;
786
+ let idle = null;
728
787
  const readRokuBuild = () => {
729
788
  try {
730
789
  return loadDevConfig(process.cwd())?.platforms?.roku?.build ?? null;
@@ -739,10 +798,25 @@ function createDevSessionController(auth, webUrl) {
739
798
  const dead = handle;
740
799
  handle = null;
741
800
  platform = null;
801
+ idle = null;
742
802
  ended = { ...e, reason: e.reason ?? dead.lapsedReason?.() ?? null };
743
803
  void dead.stop().catch(() => {
744
804
  });
745
805
  };
806
+ const markReplaced = (sessionId) => {
807
+ if (!handle || handle.sessionId !== sessionId) return;
808
+ handle = null;
809
+ platform = null;
810
+ idle = null;
811
+ ended = { sessionId, status: "replaced", reason: null };
812
+ };
813
+ let uses = 0;
814
+ const noteHeldUse = (sessionId) => {
815
+ if (!handle || handle.sessionId !== sessionId) return;
816
+ uses++;
817
+ handle.noteUse?.();
818
+ if (idle?.sessionId === sessionId) idle = null;
819
+ };
746
820
  const api = () => new ApiClient(auth.baseUrl, auth.token);
747
821
  return {
748
822
  async start(cfg) {
@@ -832,11 +906,18 @@ function createDevSessionController(auth, webUrl) {
832
906
  onCreated: (created) => {
833
907
  pending = created;
834
908
  },
835
- onDequeued: () => progress.dequeued()
909
+ onDequeued: () => progress.dequeued(),
910
+ onIdleWarning: (id, warning) => {
911
+ idle = warning ? { sessionId: id, endsAt: Date.now() + warning.seconds_remaining * 1e3 } : null;
912
+ }
836
913
  },
837
914
  (evt) => {
838
915
  progress.event(evt);
839
916
  if (evt.event === "warning") warnings.push(evt.message);
917
+ if (evt.event === "session_ended" && evt.replaced && started) {
918
+ markReplaced(evt.session_id);
919
+ return;
920
+ }
840
921
  if (evt.event === "session_ended" && evt.code !== 0 && started) {
841
922
  const id = evt.session_id;
842
923
  void api().getSession(id).then(
@@ -882,6 +963,7 @@ function createDevSessionController(auth, webUrl) {
882
963
  await handle?.stop();
883
964
  handle = null;
884
965
  platform = null;
966
+ idle = null;
885
967
  ended = null;
886
968
  },
887
969
  async shutdown() {
@@ -890,6 +972,7 @@ function createDevSessionController(auth, webUrl) {
890
972
  const starting = pending;
891
973
  handle = null;
892
974
  platform = null;
975
+ idle = null;
893
976
  pending = null;
894
977
  await Promise.all([
895
978
  held ? held.stop().then(
@@ -912,6 +995,17 @@ function createDevSessionController(auth, webUrl) {
912
995
  lastEnded() {
913
996
  return ended;
914
997
  },
998
+ noteUse: noteHeldUse,
999
+ useGeneration: () => uses,
1000
+ takeIdleWarning(sessionId) {
1001
+ const w = idle;
1002
+ if (sessionId !== void 0 && w?.sessionId !== sessionId) return null;
1003
+ idle = null;
1004
+ if (!w || !handle || handle.sessionId !== w.sessionId) return null;
1005
+ const seconds = Math.ceil((w.endsAt - Date.now()) / 1e3);
1006
+ if (seconds <= 0) return null;
1007
+ return idleNoticeText(w.sessionId, seconds);
1008
+ },
915
1009
  async reload(requested) {
916
1010
  if (reloading) {
917
1011
  throw new Error(
@@ -970,6 +1064,8 @@ function createDevSessionController(auth, webUrl) {
970
1064
  log: (line) => console.error(line)
971
1065
  });
972
1066
  await push();
1067
+ if (handle?.sessionId === target.sessionId) noteHeldUse(target.sessionId);
1068
+ else recordDevUse(target.sessionId);
973
1069
  return { sessionId: target.sessionId, seconds: (Date.now() - started) / 1e3 };
974
1070
  }
975
1071
  }
@@ -978,12 +1074,13 @@ function makeSessionResolver(devSession) {
978
1074
  return () => devSession.current()?.sessionId ?? readDevState(process.cwd())?.session_id;
979
1075
  }
980
1076
  var MCP_INSTRUCTIONS = [
981
- "VibeView runs your app on a cloud iOS simulator, Android emulator, Apple TV, Android TV or Roku (beta) device you control with these tools.",
1077
+ "VibeView runs your app on a cloud iOS simulator, Android emulator, Apple TV, Android TV or Roku device you control with these tools.",
982
1078
  "Loop: dev_start (platform, app from list_apps; add standalone: true for a release or CI build that needs no Metro) -> ui_tree -> act with refs (tap @e5, type, swipe, press) and read the change each action reports -> dev_stop when done.",
983
1079
  "Foldables: pick one by model in dev_start (list_device_models marks them foldable, with free/busy counts), then set_posture and rotate.",
984
1080
  "set_location sets the simulated GPS position (phones and tablets) for location-based flows.",
985
1081
  "screenshot writes a PNG and returns its path; pass inline: true to see the image. logs reads the app output after a crash or blank screen.",
986
- "A session stopped elsewhere is reported as ended; call dev_start again for a new one."
1082
+ "A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
1083
+ 'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
987
1084
  ].join("\n");
988
1085
  async function createMcpServer(opts) {
989
1086
  const [{ McpServer }, { z }] = await Promise.all([
@@ -1100,6 +1197,7 @@ export {
1100
1197
  deviceLine,
1101
1198
  forMcp,
1102
1199
  handleToolCall,
1200
+ idleNoticeText,
1103
1201
  mcpText,
1104
1202
  progressReporter,
1105
1203
  runMcpServer,
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-4D2E2PE4.js";
6
+ } from "./chunk-QRJAFBH3.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.34.0",
4
- "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
3
+ "version": "0.36.0",
4
+ "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
7
7
  "bin": {
@@ -211,7 +211,7 @@ appear unexplained in their `git status`.
211
211
  `ui-tree` afterwards — old refs are stale. On other phones and tablets
212
212
  `rotate` toggles portrait/landscape (only 90 is accepted); TV devices don't
213
213
  rotate.
214
- - **Roku channels (beta)**: start the session with
214
+ - **Roku channels**: start the session with
215
215
  `vibeview dev --platform roku --detach` (or `dev_start` with platform
216
216
  `roku` over MCP); after an edit run `vibeview dev-reload` (MCP:
217
217
  `dev_reload`) to re-package, upload and restart the channel in the same
@@ -254,6 +254,10 @@ vibeview dev-stop
254
254
  If the `dev --detach` process is killed or the computer sleeps, its session
255
255
  ends on its own within about 90 seconds (unless a browser tab is watching
256
256
  it) — but don't rely on that; stop it.
257
+ A session nobody uses also ends on its own after the organization's idle
258
+ timeout (30 minutes by default). Only device actions (tap, type, swipe,
259
+ press, …) and app reloads count as use; `ui-tree`, `screenshot`, `logs`,
260
+ `find` and `wait` do not, and neither does keeping `dev --detach` running.
257
261
  A session you did not start with `dev` (one created through the API, the
258
262
  sandbox, or a collaboration link) is stopped by id:
259
263
  ```bash
@@ -332,6 +336,10 @@ steps the CLI does with plain shell commands:
332
336
  If the held session ends elsewhere (dashboard, idle timeout, failure), the
333
337
  next tool call answers "session … has ended (…) — call dev_start"; the
334
338
  server has already let it go, so `dev_start` starts a fresh one.
339
+ A tool result that ends with `Session … ends in N s for inactivity` means
340
+ the held session is about to time out: act on the device to keep it, or call
341
+ `dev_stop` if you are done. A session another VibeView dev connection took
342
+ over is reported as taken over; it keeps running there, so don't stop it.
335
343
 
336
344
  **`dev_start` is stateful — it changes the default session for every later
337
345
  call.** Once it succeeds, any tool called without an explicit `session_id`