omp-mobile 0.5.1 → 0.6.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/package.json +1 -1
- package/src/attention.mjs +6 -1
- package/src/rpc-session.mjs +4 -1
- package/src/server.mjs +42 -6
- package/src/transcripts.mjs +1 -1
package/package.json
CHANGED
package/src/attention.mjs
CHANGED
|
@@ -31,6 +31,8 @@ import { hostname } from "node:os";
|
|
|
31
31
|
export const HEARTBEAT_MS = 25_000;
|
|
32
32
|
/** OMP's tool approval is a `select` whose title starts with this. */
|
|
33
33
|
export const TOOL_APPROVAL_PREFIX = "Allow tool:";
|
|
34
|
+
/** The labels OMP starts approval details with (`Command: `, `Path: `), from its `tools/approval.ts` and each tool. */
|
|
35
|
+
const DETAIL_LABEL = /^(?:Command|Path|Paths|Pattern|Language|Code|Content|URL|Reason|Origin):\s*/;
|
|
34
36
|
const ASKING = new Set(["select", "confirm", "input", "editor"]);
|
|
35
37
|
const TEXT_MAX = 280;
|
|
36
38
|
|
|
@@ -127,7 +129,10 @@ export class AttentionTracker {
|
|
|
127
129
|
const approval = isToolApproval(request);
|
|
128
130
|
const title = typeof request.title === "string" ? request.title : "";
|
|
129
131
|
// OMP's tool approval title is `Allow tool: bash\nCommand: echo hi`: the tool, then its details.
|
|
130
|
-
|
|
132
|
+
// A notification reads in the phone's language, so OMP's English labels (`Command:`) are left out.
|
|
133
|
+
const lines = (approval ? title.slice(TOOL_APPROVAL_PREFIX.length) : title)
|
|
134
|
+
.split("\n")
|
|
135
|
+
.map((line, index) => (approval && index > 0 ? line.replace(DETAIL_LABEL, "") : line).trim());
|
|
131
136
|
const text = [...lines, request.message].filter(Boolean).join(" · ");
|
|
132
137
|
state.open.set(request.id, emit(approval ? "approval" : "question", request.id, text));
|
|
133
138
|
break;
|
package/src/rpc-session.mjs
CHANGED
|
@@ -400,11 +400,14 @@ export class RpcSession extends EventEmitter {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
/**
|
|
403
|
+
* Send a prompt. While a turn runs it is queued as a steer, as Enter in the
|
|
404
|
+
* terminal does: OMP delivers it at the next step boundary (the queue chips
|
|
405
|
+
* show it meanwhile). Idle, it starts a turn as before.
|
|
403
406
|
* @param {string} text
|
|
404
407
|
* @param {{type: "image", data: string, mimeType: string}[]} [images] OMP `ImageContent` parts sent with the text.
|
|
405
408
|
*/
|
|
406
409
|
async prompt(text, images = []) {
|
|
407
|
-
return this.request("prompt", images.length > 0 ? {
|
|
410
|
+
return this.request("prompt", { message: text, ...(images.length > 0 ? { images } : {}), streamingBehavior: "steer" });
|
|
408
411
|
}
|
|
409
412
|
|
|
410
413
|
async abort() {
|
package/src/server.mjs
CHANGED
|
@@ -119,7 +119,8 @@ const OPEN_FAILED_CLOSE_CODE = 4000;
|
|
|
119
119
|
const BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
120
120
|
|
|
121
121
|
/** @typedef {{pid: number, busy: boolean, terminal: boolean}} SessionOwner */
|
|
122
|
-
/** @typedef {{id: string,
|
|
122
|
+
/** @typedef {{id: string, method: string, title: string, message?: string, options?: string[]}} PendingRequest */
|
|
123
|
+
/** @typedef {{id: string, title: string, cwd: string, status: string, model?: string, createdAt: number, updatedAt: number, sessionFile?: string, origin: "daemon" | "omp", openElsewhere?: boolean, owner?: SessionOwner, waiting?: number, request?: PendingRequest}} SessionSummary */
|
|
123
124
|
|
|
124
125
|
/** An error with the HTTP status it answers, and any detail its JSON answer carries. */
|
|
125
126
|
function httpError(status, message, detail = {}) {
|
|
@@ -132,6 +133,29 @@ function errorBody(error) {
|
|
|
132
133
|
return { error: error instanceof Error ? error.message : String(error), ...detail };
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The part of an open extension request a session list shows: enough to answer
|
|
138
|
+
* a tool approval or a short choice from the home, nothing for a text prompt.
|
|
139
|
+
* @param {any} request
|
|
140
|
+
* @returns {PendingRequest | undefined}
|
|
141
|
+
*/
|
|
142
|
+
function pendingOf(request) {
|
|
143
|
+
if (request === null || typeof request !== "object" || typeof request.id !== "string") return undefined;
|
|
144
|
+
const pending = { id: request.id, method: String(request.method), title: typeof request.title === "string" ? request.title : "" };
|
|
145
|
+
if (typeof request.message === "string") pending.message = request.message;
|
|
146
|
+
if (Array.isArray(request.options)) pending.options = request.options.filter((option) => typeof option === "string");
|
|
147
|
+
return pending;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The answer fields a phone sends, over the session socket or `POST /sessions/:id/answer`. */
|
|
151
|
+
function answerOf(message) {
|
|
152
|
+
const answer = {};
|
|
153
|
+
if (typeof message.value === "string") answer.value = message.value;
|
|
154
|
+
if (typeof message.confirmed === "boolean") answer.confirmed = message.confirmed;
|
|
155
|
+
if (message.cancelled === true) answer.cancelled = true;
|
|
156
|
+
return answer;
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
function parseArgs(argv) {
|
|
136
160
|
const options = {
|
|
137
161
|
host: "127.0.0.1",
|
|
@@ -631,6 +655,8 @@ class SessionEntry {
|
|
|
631
655
|
origin: "daemon",
|
|
632
656
|
// OMP questions and tool approvals still waiting for an answer: the session needs the user.
|
|
633
657
|
waiting: this.openRequests().length,
|
|
658
|
+
// The oldest of them, so a list can answer it without opening the session.
|
|
659
|
+
request: pendingOf(this.openRequests()[0]),
|
|
634
660
|
};
|
|
635
661
|
}
|
|
636
662
|
|
|
@@ -683,6 +709,8 @@ class SessionEntry {
|
|
|
683
709
|
if (STATE_EVENTS.has(event.type)) void this.refreshState();
|
|
684
710
|
// A `todo` tool call rewrites the list before any reminder fires.
|
|
685
711
|
if (event.type === "tool_execution_end" && event.toolName === "todo") void this.refreshState();
|
|
712
|
+
// A run starting is what makes a session "running" in the list; OMP's state read would lag behind.
|
|
713
|
+
if (event.type === "agent_start" && this.state !== null && typeof this.state === "object") this.state = { ...this.state, isStreaming: true };
|
|
686
714
|
if (event.type === "turn_end" || event.type === "agent_end") void this.refreshState();
|
|
687
715
|
this.broadcast({ type: "event", event });
|
|
688
716
|
});
|
|
@@ -922,6 +950,18 @@ class Daemon {
|
|
|
922
950
|
sendJson(res, 200, this.transcript(decodeURIComponent(transcript[1]), deltaQuery(url.searchParams)));
|
|
923
951
|
return;
|
|
924
952
|
}
|
|
953
|
+
const answer = /^\/sessions\/([^/]+)\/answer$/.exec(url.pathname);
|
|
954
|
+
if (answer !== null && req.method === "POST") {
|
|
955
|
+
const body = await readBody(req);
|
|
956
|
+
const live = this.sessions.get(decodeURIComponent(answer[1]));
|
|
957
|
+
if (live === undefined) throw httpError(404, "session is not running");
|
|
958
|
+
if (typeof body.id !== "string" || !live.openRequests().some((request) => request.id === body.id)) {
|
|
959
|
+
throw httpError(409, "that question was already answered");
|
|
960
|
+
}
|
|
961
|
+
live.rpc.respondExtension(body.id, answerOf(body));
|
|
962
|
+
sendJson(res, 200, { ok: true });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
925
965
|
const match = /^\/sessions\/([^/]+)$/.exec(url.pathname);
|
|
926
966
|
if (match !== null) {
|
|
927
967
|
const id = decodeURIComponent(match[1]);
|
|
@@ -1318,11 +1358,7 @@ class Daemon {
|
|
|
1318
1358
|
return;
|
|
1319
1359
|
case "approval": {
|
|
1320
1360
|
if (typeof message.id !== "string") throw new Error("missing approval id");
|
|
1321
|
-
|
|
1322
|
-
if (typeof message.value === "string") answer.value = message.value;
|
|
1323
|
-
if (typeof message.confirmed === "boolean") answer.confirmed = message.confirmed;
|
|
1324
|
-
if (message.cancelled === true) answer.cancelled = true;
|
|
1325
|
-
entry.rpc.respondExtension(message.id, answer);
|
|
1361
|
+
entry.rpc.respondExtension(message.id, answerOf(message));
|
|
1326
1362
|
ws.send(JSON.stringify({ type: "accepted", command: "approval" }));
|
|
1327
1363
|
return;
|
|
1328
1364
|
}
|
package/src/transcripts.mjs
CHANGED
|
@@ -136,7 +136,7 @@ export class TranscriptStore {
|
|
|
136
136
|
base: Number.isSafeInteger(base) && base > 0 ? base : 0,
|
|
137
137
|
messages,
|
|
138
138
|
state: state !== null && typeof state === "object" ? { ...state, isStreaming: false } : state,
|
|
139
|
-
session: { ...session, status: "idle", waiting: 0 },
|
|
139
|
+
session: { ...session, status: "idle", waiting: 0, request: undefined },
|
|
140
140
|
};
|
|
141
141
|
try {
|
|
142
142
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|