dsh-phone-bridge 0.2.4 → 0.2.5

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/index.js +79 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,6 +38,7 @@ trash panel at the bottom of the sidebar where each entry can be restored or pur
38
38
  | `GET /models` | the deployment model catalog |
39
39
  | `POST /model` | switch the model a session uses |
40
40
  | `GET /health` | whether this DSH build still has every `sessionController` method the plugin calls, and which are missing if not |
41
+ | `GET /recent?sessionId=&limit=` | the user's own last few messages in a session, read through `sessionController.page()` |
41
42
 
42
43
  Deleting **moves** a session to `~/.dsh/deleted-sessions/` instead of unlinking it,
43
44
  so a delete stays recoverable and purging is a separate, explicit step.
package/index.js CHANGED
@@ -237,6 +237,7 @@ const REQUIRED_CONTROLLER_METHODS = [
237
237
  "inspect",
238
238
  "list",
239
239
  "modelCatalog",
240
+ "page",
240
241
  "prompt",
241
242
  "rename",
242
243
  "search",
@@ -273,6 +274,27 @@ function listOnDiskSessionIds() {
273
274
  return ids;
274
275
  }
275
276
 
277
+ // Plain text out of a message's content blocks.
278
+ //
279
+ // The block shape is not a stable surface either, so only text-ish fields are
280
+ // read and anything unrecognised is skipped rather than stringified into the
281
+ // output as "[object Object]".
282
+ function textFromContentBlocks(content) {
283
+ if (typeof content === "string") return content.trim();
284
+ if (!Array.isArray(content)) return "";
285
+ const parts = [];
286
+ for (const block of content) {
287
+ if (typeof block === "string") {
288
+ if (block.trim()) parts.push(block.trim());
289
+ continue;
290
+ }
291
+ if (!block || typeof block !== "object") continue;
292
+ const text = [block.text, block.content].find((v) => typeof v === "string" && v.trim());
293
+ if (text) parts.push(text.trim());
294
+ }
295
+ return parts.join("\n").trim();
296
+ }
297
+
276
298
  // Pull just the title out of a session's projection bag.
277
299
  //
278
300
  // The bag also carries token counts, context pressure, the turn outline and the
@@ -900,6 +922,63 @@ export function apply(ctx, config = {}) {
900
922
  // between releases; when it does, the symptom would otherwise be a bare 500 on
901
923
  // whichever route happens to touch the missing method, with nothing said about
902
924
  // why. Reporting it up front turns that into one clear line.
925
+ // The user's own last few messages in a session, so the phone can answer "where
926
+ // did this conversation get to" without scrolling a chat app.
927
+ //
928
+ // Read through sessionController.page(), which is the supported history read and
929
+ // does not activate a cold session. The event shape is not a stable surface, so
930
+ // every field access is guarded and an unknown payload degrades to fewer lines
931
+ // rather than throwing.
932
+ disposers.push(ctx.webServer.register({
933
+ kind: "exact",
934
+ path: `${ROUTE_PATH}/recent`,
935
+ handler: async (req, res) => {
936
+ try {
937
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
938
+ const sessionId = (url.searchParams.get("sessionId") ?? "").trim();
939
+ const limit = Math.min(Math.max(Number(url.searchParams.get("limit")) || 3, 1), 20);
940
+ if (!sessionId) {
941
+ sendJson(res, 400, { ok: false, error: "sessionId is required" });
942
+ return;
943
+ }
944
+ const sessionController = ctx.get("sessionController");
945
+ if (!sessionController) throw new Error("sessionController is unavailable");
946
+ if (typeof sessionController.page !== "function") {
947
+ throw new Error("this DSH build does not expose sessionController.page");
948
+ }
949
+
950
+ const signal = new AbortController().signal;
951
+ // throughSeq: -1 means "the newest page"; maxMessages stays well above the
952
+ // requested count because assistant and tool events sit between the user's.
953
+ const page = await sessionController.page({
954
+ address: { kind: "session", sessionId },
955
+ throughSeq: -1,
956
+ maxMessages: 200,
957
+ }, signal);
958
+
959
+ const texts = [];
960
+ for (const record of page?.records ?? []) {
961
+ const event = record?.event;
962
+ if (event?.type !== "user/message") continue;
963
+ if (event?.data?.source?.kind !== "user") continue;
964
+ const text = textFromContentBlocks(event.data.content);
965
+ if (!text) continue;
966
+ texts.push({ at: event.time ?? null, text });
967
+ }
968
+
969
+ sendJson(res, 200, {
970
+ ok: true,
971
+ sessionId,
972
+ total: texts.length,
973
+ // Newest last, so the caller can slice from the end.
974
+ messages: texts.slice(-limit),
975
+ });
976
+ } catch (error) {
977
+ sendJson(res, 500, { ok: false, error: String(error?.message ?? error) });
978
+ }
979
+ },
980
+ }));
981
+
903
982
  disposers.push(ctx.webServer.register({
904
983
  kind: "exact",
905
984
  path: `${ROUTE_PATH}/health`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-phone-bridge",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Drive an existing DSH desktop session from a chat app, plus a sidebar delete button and trash panel",
5
5
  "type": "module",
6
6
  "main": "index.js",