omp-mobile 0.4.3 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-mobile",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "The computer side of OMP Mobile: runs oh-my-pi sessions and serves them to the phone app over Tailscale.",
5
5
  "keywords": [
6
6
  "mobile",
@@ -21,7 +21,7 @@ import { homedir } from "node:os";
21
21
  import { delimiter, join } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { renderQr } from "./qr.mjs";
24
- import { requestPath } from "./takeover.mjs";
24
+ import { requestPath, shellWord } from "./takeover.mjs";
25
25
 
26
26
  const HOME = join(homedir(), ".omp-mobile");
27
27
  const STATE_FILE = join(HOME, "daemon.json");
@@ -229,7 +229,7 @@ export default function ompMobile(pi) {
229
229
  rmSync(requestPath(file), { force: true });
230
230
  // OMP closes once the session is idle; the daemon signals it if that does not come soon,
231
231
  // and tells the terminal where the session went once omp is gone.
232
- ctx.ui.notify(`OMP Mobile: this session continues on ${request.device}. Closing omp here…`, "warning");
232
+ ctx.ui.notify(`OMP Mobile: this session continues on ${request.device}. Closing omp here; resume it later with: omp --resume ${shellWord(file)}`, "warning");
233
233
  if (!ctx.isIdle()) {
234
234
  ctx.abort();
235
235
  const deadline = Date.now() + TAKEOVER_IDLE_WAIT_MS;
package/src/server.mjs CHANGED
@@ -58,8 +58,6 @@ const IDLE_SESSION_MS = 30 * 60_000;
58
58
  const REAP_INTERVAL_MS = 60_000;
59
59
  /** Messages a live session keeps in memory; older ones leave its front (its `base` counts them). */
60
60
  const MAX_LIVE_MESSAGES = 5_000;
61
- /** Events that are a session being used: a turn and what it streams. State and setting echoes are not. */
62
- const ACTIVITY_EVENT = /^(agent_|turn_|message_|tool_execution_|auto_compaction_|auto_retry_)/;
63
61
  /**
64
62
  * The fields of a tool result's `details` the app reads: an edit's diff, a
65
63
  * read's line count, a command's run time, a screenshot's file. The rest
@@ -638,8 +636,10 @@ class SessionEntry {
638
636
 
639
637
  bind() {
640
638
  this.rpc.on("event", (event) => {
641
- if (typeof event?.type === "string" && ACTIVITY_EVENT.test(event.type)) this.updatedAt = Date.now();
642
639
  if (event.type === "message_end" && event.message) {
640
+ // A new message is what makes a session the most recent one; opening it,
641
+ // a lifecycle event from a resumed omp, or a state echo are not.
642
+ this.updatedAt = Date.now();
643
643
  this.messages.push(phoneMessage(event.message, this.blobDir));
644
644
  if (this.messages.length > MAX_LIVE_MESSAGES) {
645
645
  // Indices stay the transcript's: a phone's `since` still lines up after the front is dropped.
@@ -1125,7 +1125,7 @@ class Daemon {
1125
1125
  const deadline = Date.now() + 3_000;
1126
1126
  while ((await this.owners())?.get(id) === pid && Date.now() < deadline) await new Promise((wait) => setTimeout(wait, 100));
1127
1127
  // Whoever looks at the terminal sees why omp closed.
1128
- if (info?.tty !== undefined) tellTerminal(info.tty, takeoverFarewell(device));
1128
+ if (info?.tty !== undefined) tellTerminal(info.tty, takeoverFarewell({ device, sessionFile: stored.sessionFile }));
1129
1129
  const forks = forksOf(dirname(stored.sessionFile), id, since);
1130
1130
  if (forks.length > 0) this.log(new Error(`taking over ${id} from pid ${pid} left forked session files: ${forks.join(", ")}`));
1131
1131
  outcome = { pid, via: left.via, waitedMs: left.waitedMs, busy, forks };
@@ -1253,6 +1253,8 @@ class Daemon {
1253
1253
  const text = typeof message.text === "string" ? message.text : "";
1254
1254
  if (text.trim() === "" && images.length === 0) throw new Error("empty prompt");
1255
1255
  await entry.rpc.prompt(text, images);
1256
+ entry.updatedAt = Date.now();
1257
+ entry.onChange?.(entry);
1256
1258
  ws.send(JSON.stringify({ type: "accepted", command: "prompt" }));
1257
1259
  return;
1258
1260
  }
@@ -1284,7 +1286,6 @@ class Daemon {
1284
1286
  }
1285
1287
  const state = await entry.rpc.setModel(message.provider, message.modelId);
1286
1288
  entry.state = state;
1287
- entry.updatedAt = Date.now();
1288
1289
  entry.onChange?.(entry);
1289
1290
  entry.broadcast({ type: "state", state });
1290
1291
  ws.send(JSON.stringify({ type: "accepted", command: "set_model" }));
@@ -1301,7 +1302,6 @@ class Daemon {
1301
1302
  if (typeof message.level !== "string" || message.level === "") throw new Error("set_thinking_level needs a level");
1302
1303
  const state = await entry.rpc.setThinkingLevel(message.level);
1303
1304
  entry.state = state;
1304
- entry.updatedAt = Date.now();
1305
1305
  entry.broadcast({ type: "state", state });
1306
1306
  ws.send(JSON.stringify({ type: "accepted", command: "set_thinking_level" }));
1307
1307
  return;
package/src/takeover.mjs CHANGED
@@ -175,9 +175,21 @@ export function tellTerminal(tty, text) {
175
175
  }
176
176
  }
177
177
 
178
- /** What the terminal is told once omp left it. */
179
- export function takeoverFarewell(device) {
180
- return `omp-mobile: this session continues on ${device}. omp closed here so the phone could take it over.`;
178
+ /** A path as a shell word: quoted only when it has to be. */
179
+ export function shellWord(value) {
180
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${String(value).replace(/'/g, `'\\''`)}'`;
181
+ }
182
+
183
+ /**
184
+ * What the terminal is told once omp left it: where the session went, and the
185
+ * command that brings it back -- the hint leaving omp by hand also leaves.
186
+ * @param {{device: string, sessionFile: string}} leaving
187
+ */
188
+ export function takeoverFarewell({ device, sessionFile }) {
189
+ const lines = [`This session continues on the phone (${device}).`, "omp closed here so the phone could take it over."];
190
+ const file = typeof sessionFile === "string" && sessionFile !== "" ? shellWord(sessionFile) : "";
191
+ if (file !== "") lines.push("", "Resume it in this terminal with:", ` omp --resume ${file}`);
192
+ return lines.join("\n");
181
193
  }
182
194
 
183
195
  const sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));