pinomad 0.11.0 → 0.13.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.
Files changed (103) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
  3. package/dist/{chunk-jt34g8st.js → chunk-4fferbxw.js} +247 -27
  4. package/dist/{chunk-67gq282t.js → chunk-53xv3fny.js} +725 -75
  5. package/dist/{chunk-44pnts98.js → chunk-563fnad9.js} +1 -1
  6. package/dist/{chunk-b457q0tm.js → chunk-6th9xv39.js} +11 -8
  7. package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
  8. package/dist/{chunk-as6pyf00.js → chunk-90g1cg9n.js} +1 -1
  9. package/dist/{chunk-9h5vawnm.js → chunk-aapsc9g0.js} +270 -66
  10. package/dist/{chunk-n1m1tjrk.js → chunk-d4tq62yr.js} +57 -16
  11. package/dist/{chunk-gtdpjqa3.js → chunk-d9mq5d46.js} +205 -49
  12. package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
  13. package/dist/{chunk-xqycs43g.js → chunk-kmzzjv53.js} +83 -8
  14. package/dist/{chunk-4dcs8sbj.js → chunk-m51mq8vh.js} +43 -21
  15. package/dist/{chunk-tj57aqav.js → chunk-maqv34cs.js} +2 -2
  16. package/dist/chunk-nq57fdnq.js +106 -0
  17. package/dist/{chunk-h5q2y57m.js → chunk-ra86wjp5.js} +10 -4
  18. package/dist/{chunk-sd5fkxjj.js → chunk-s0h7c7da.js} +1 -1
  19. package/dist/chunk-sd640dt7.js +22 -0
  20. package/dist/{chunk-6d16c2fv.js → chunk-t6fw0wa9.js} +13 -6
  21. package/dist/{chunk-vvj9b1gc.js → chunk-wd7aracm.js} +188 -54
  22. package/dist/pinomad.js +15 -14
  23. package/package.json +13 -11
  24. package/web/assets/astro-DOly5024.js +1 -0
  25. package/web/assets/c-BIGW1oBm.js +1 -0
  26. package/web/assets/core-BHm2gdHL.js +4 -0
  27. package/web/assets/cpp-DddhVWhK.js +1 -0
  28. package/web/assets/csharp-B_TYUkTI.js +1 -0
  29. package/web/assets/css-Z72CF80r.js +1 -0
  30. package/web/assets/dart-CtKP3W-j.js +1 -0
  31. package/web/assets/diff-D97Zzqfu.js +1 -0
  32. package/web/assets/dockerfile-BcOcwvcX.js +1 -0
  33. package/web/assets/elixir-DkJLSxtU.js +1 -0
  34. package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
  35. package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
  36. package/web/assets/fish-BvzEVeQv.js +1 -0
  37. package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  38. package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  39. package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  40. package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  41. package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  42. package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  43. package/web/assets/go-C27-OAKa.js +1 -0
  44. package/web/assets/graphql-ChdNCCLP.js +1 -0
  45. package/web/assets/haskell-Df6bDoY_.js +1 -0
  46. package/web/assets/hcl-BWvSN4gD.js +1 -0
  47. package/web/assets/highlighted-diff-DrHvSLK9.js +175 -0
  48. package/web/assets/highlights-1aLwssz-.js +4 -0
  49. package/web/assets/highlights-themes-C1p4pFYS.js +1 -0
  50. package/web/assets/html-BlS82ilI.js +1 -0
  51. package/web/assets/index-A_AUJoO6.css +1 -0
  52. package/web/assets/index-BafYMKiN.js +396 -0
  53. package/web/assets/ini-BEwlwnbL.js +1 -0
  54. package/web/assets/java-CylS5w8V.js +1 -0
  55. package/web/assets/javascript-wDzz0qaB.js +1 -0
  56. package/web/assets/json-Cp-IABpG.js +1 -0
  57. package/web/assets/jsonc-Des-eS-w.js +1 -0
  58. package/web/assets/jsx-g9-lgVsj.js +1 -0
  59. package/web/assets/kotlin-BdnUsdx6.js +1 -0
  60. package/web/assets/less-B1dDrJ26.js +1 -0
  61. package/web/assets/lua-BaeVxFsk.js +1 -0
  62. package/web/assets/make-CHLpvVh8.js +1 -0
  63. package/web/assets/markdown-Cvjx9yec.js +1 -0
  64. package/web/assets/nix-CwoSXNpI.js +1 -0
  65. package/web/assets/objective-c-DXmwc3jG.js +1 -0
  66. package/web/assets/php-DoruYMF0.js +1 -0
  67. package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
  68. package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
  69. package/web/assets/pierre-light-480U9XYS.js +1 -0
  70. package/web/assets/pierre-light-BVSW-Xev.js +1 -0
  71. package/web/assets/postcss-DnzrXTVv.js +1 -0
  72. package/web/assets/powershell-Dd0kTxKP.js +1 -0
  73. package/web/assets/prisma-Vru482bI.js +1 -0
  74. package/web/assets/proto-C7zT0LnQ.js +1 -0
  75. package/web/assets/python-B6aJPvgy.js +1 -0
  76. package/web/assets/ruby-B43yHlf9.js +1 -0
  77. package/web/assets/rust-DPvrY3IQ.js +1 -0
  78. package/web/assets/scala-CqE71os6.js +1 -0
  79. package/web/assets/scss-Cw6XMfVs.js +1 -0
  80. package/web/assets/shellscript-Yzrsuije.js +1 -0
  81. package/web/assets/shiki-BwyCm3kZ.js +2 -0
  82. package/web/assets/shiki-langs-CMlALLSg.js +2 -0
  83. package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
  84. package/web/assets/sql-DkvoVjww.js +1 -0
  85. package/web/assets/stream-tokenizer-BlgIhw0y.js +4 -0
  86. package/web/assets/svelte-DoIXUgJc.js +1 -0
  87. package/web/assets/swift-C2oV4EkX.js +1 -0
  88. package/web/assets/terraform-BETggiCN.js +1 -0
  89. package/web/assets/theming-themes-DDvoqEqT.js +1 -0
  90. package/web/assets/toml-vGWfd6FD.js +1 -0
  91. package/web/assets/tsx-COt5Ahok.js +1 -0
  92. package/web/assets/typescript-BPQ3VLAy.js +1 -0
  93. package/web/assets/vue-CwSeJimo.js +1 -0
  94. package/web/assets/xml-sdJ4AIDG.js +1 -0
  95. package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
  96. package/web/assets/xterm-view-BnKOnF3C.js +38 -0
  97. package/web/assets/yaml-Buea-lGh.js +1 -0
  98. package/web/assets/zig-VOosw3JB.js +1 -0
  99. package/web/index.html +2 -2
  100. package/dist/chunk-etf7ef97.js +0 -55
  101. package/dist/chunk-greb3dp6.js +0 -22
  102. package/web/assets/index-BhHrKpoo.js +0 -395
  103. package/web/assets/index-DoyHMPwf.css +0 -1
@@ -5,7 +5,7 @@ import {
5
5
  openHostSettings,
6
6
  sameList,
7
7
  settingsDefaults
8
- } from "./chunk-44pnts98.js";
8
+ } from "./chunk-563fnad9.js";
9
9
  import {
10
10
  DevicesDoc,
11
11
  RevocationsDoc,
@@ -16,19 +16,21 @@ import {
16
16
  pairingOffers
17
17
  } from "./chunk-t91zt5f0.js";
18
18
  import {
19
+ REMOVAL_NOTICE_FILE,
20
+ markRemoved,
19
21
  normalizeOrigin,
20
22
  readServerBinding,
21
23
  readServerConfig,
22
24
  resolveRelayOrigin
23
- } from "./chunk-etf7ef97.js";
25
+ } from "./chunk-nq57fdnq.js";
24
26
  import {
25
27
  serveWebClient
26
- } from "./chunk-wb10efyq.js";
28
+ } from "./chunk-445cevpm.js";
27
29
  import {
28
30
  relayHostId,
29
31
  relayWsBase,
30
32
  startRelayLink
31
- } from "./chunk-y127fe1a.js";
33
+ } from "./chunk-g3ahp7b9.js";
32
34
  import {
33
35
  ATTACHMENT_TTL_MS,
34
36
  INVALID_FRAME_CLOSE_CODE,
@@ -36,6 +38,7 @@ import {
36
38
  MAX_ATTACHMENT_NAME_LENGTH,
37
39
  MAX_PENDING_ATTACHMENTS,
38
40
  MAX_PENDING_ATTACHMENT_BYTES,
41
+ MAX_TRANSCRIPT_PAGE,
39
42
  PROTOCOL,
40
43
  RTC_DATA_CHANNEL_LABEL,
41
44
  RTC_DEFAULT_STUN,
@@ -54,7 +57,7 @@ import {
54
57
  rtcCandidateType,
55
58
  sdpCandidates,
56
59
  withRtcCandidates
57
- } from "./chunk-xqycs43g.js";
60
+ } from "./chunk-kmzzjv53.js";
58
61
  import {
59
62
  ENDPOINT_SET_SCHEMA_VERSION,
60
63
  MAX_HOST_ENDPOINT_URL_LENGTH,
@@ -77,13 +80,13 @@ import {
77
80
  import {
78
81
  packagedVersion,
79
82
  webRoot
80
- } from "./chunk-sd5fkxjj.js";
83
+ } from "./chunk-s0h7c7da.js";
81
84
  import {
82
85
  __require
83
86
  } from "./chunk-df32avw6.js";
84
87
 
85
88
  // apps/host/src/main.ts
86
- import { randomUUID as randomUUID3 } from "node:crypto";
89
+ import { randomUUID as randomUUID4 } from "node:crypto";
87
90
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
88
91
  import { homedir as homedir3 } from "node:os";
89
92
  import { join as join12, resolve as resolve7 } from "node:path";
@@ -176,8 +179,10 @@ var QuestionWait = defineTask({
176
179
  resolve(initial);
177
180
  return;
178
181
  }
179
- watch.start(async (value) => {
180
- const found = resolutionOf(value, task.input.requestId);
182
+ watch.start(async (call) => {
183
+ if (call.kind !== "value")
184
+ return;
185
+ const found = resolutionOf(call.value, task.input.requestId);
181
186
  if (found !== undefined)
182
187
  resolve(found);
183
188
  });
@@ -712,11 +717,7 @@ async function checkoutInfo(harness, input, context) {
712
717
  }
713
718
  };
714
719
  }
715
- async function cleanupArchivedWorktrees(harness, rootId, context) {
716
- const index = await harness.snapshot(IndexDoc, context);
717
- const checkouts = index?.checkouts ?? [];
718
- if (checkouts.length === 0)
719
- return [];
720
+ async function conversationSubtree(harness, rootId, context) {
720
721
  const all = [];
721
722
  let cursor;
722
723
  do {
@@ -743,6 +744,27 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
743
744
  }
744
745
  }
745
746
  }
747
+ return subtree;
748
+ }
749
+ async function familyRoot(harness, id, context) {
750
+ let current = id;
751
+ const seen = new Set;
752
+ while (!seen.has(current)) {
753
+ seen.add(current);
754
+ const record = await harness.commit((tx) => tx.conversation(current), context);
755
+ const parent = record?.parent?.conversationId ?? record?.owner?.conversationId;
756
+ if (parent === undefined)
757
+ return current;
758
+ current = parent;
759
+ }
760
+ return current;
761
+ }
762
+ async function cleanupArchivedWorktrees(harness, rootId, context) {
763
+ const index = await harness.snapshot(IndexDoc, context);
764
+ const checkouts = index?.checkouts ?? [];
765
+ if (checkouts.length === 0)
766
+ return [];
767
+ const subtree = await conversationSubtree(harness, rootId, context);
746
768
  const kept = [];
747
769
  for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
748
770
  const reason = await removeWorktree(record);
@@ -1451,6 +1473,7 @@ var todo = {
1451
1473
  };
1452
1474
 
1453
1475
  // apps/host/src/gateway.ts
1476
+ import { stat as stat6 } from "node:fs/promises";
1454
1477
  import { createServer } from "node:http";
1455
1478
  import { homedir, networkInterfaces } from "node:os";
1456
1479
  import { join as join7, sep as sep5 } from "node:path";
@@ -2056,6 +2079,329 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2056
2079
  };
2057
2080
  }
2058
2081
 
2082
+ // apps/host/src/terminals.ts
2083
+ import { randomUUID as randomUUID2 } from "node:crypto";
2084
+ import { userInfo } from "node:os";
2085
+ var TERMINAL_LIMIT = 8;
2086
+ var BUFFER_SIZE = 512 * 1024;
2087
+ var FLUSH_MS = 8;
2088
+ var MAX_CHUNK = 16 * 1024;
2089
+ var MAX_TITLE = 1024;
2090
+ var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
2091
+
2092
+ class Terminals {
2093
+ #options;
2094
+ #limit;
2095
+ #terminals = new Map;
2096
+ #numbers = new Map;
2097
+ #watchers = new Map;
2098
+ constructor(options) {
2099
+ this.#options = options;
2100
+ this.#limit = options.limit ?? TERMINAL_LIMIT;
2101
+ }
2102
+ get limit() {
2103
+ return this.#limit;
2104
+ }
2105
+ list(owner) {
2106
+ const terminals = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner).map(info);
2107
+ return { owner, limit: this.#limit, terminals };
2108
+ }
2109
+ watch(owner, listener) {
2110
+ let set = this.#watchers.get(owner);
2111
+ if (set === undefined)
2112
+ this.#watchers.set(owner, set = new Set);
2113
+ set.add(listener);
2114
+ listener(this.list(owner));
2115
+ return () => {
2116
+ set.delete(listener);
2117
+ if (set.size === 0)
2118
+ this.#watchers.delete(owner);
2119
+ };
2120
+ }
2121
+ open(owner, cwd, size) {
2122
+ const count = [...this.#terminals.values()].filter((terminal2) => terminal2.owner === owner).length;
2123
+ if (count >= this.#limit)
2124
+ throw new Error(`Terminal limit reached (${this.#limit})`);
2125
+ const number = (this.#numbers.get(owner) ?? 0) + 1;
2126
+ this.#numbers.set(owner, number);
2127
+ const terminal = {
2128
+ id: randomUUID2(),
2129
+ owner,
2130
+ cwd,
2131
+ number,
2132
+ pty: undefined,
2133
+ generation: 0,
2134
+ cols: size.cols,
2135
+ rows: size.rows,
2136
+ title: undefined,
2137
+ rename: undefined,
2138
+ exit: undefined,
2139
+ buffer: "",
2140
+ start: 0,
2141
+ pending: "",
2142
+ flushTimer: undefined,
2143
+ titleCarry: "",
2144
+ viewers: new Set
2145
+ };
2146
+ this.#terminals.set(terminal.id, terminal);
2147
+ try {
2148
+ this.#start(terminal);
2149
+ } catch (error) {
2150
+ this.#terminals.delete(terminal.id);
2151
+ throw error;
2152
+ }
2153
+ this.#changed(owner);
2154
+ return terminal.id;
2155
+ }
2156
+ input(id, data, size) {
2157
+ const terminal = this.#get(id);
2158
+ if (size !== undefined)
2159
+ this.#resize(terminal, size);
2160
+ terminal.pty?.write(data);
2161
+ }
2162
+ resize(id, size) {
2163
+ this.#resize(this.#get(id), size);
2164
+ }
2165
+ rename(id, name) {
2166
+ const terminal = this.#get(id);
2167
+ const trimmed = name?.trim();
2168
+ terminal.rename = trimmed === undefined || trimmed === "" ? undefined : trimmed;
2169
+ this.#changed(terminal.owner);
2170
+ }
2171
+ restart(id, size) {
2172
+ const terminal = this.#get(id);
2173
+ this.#stop(terminal);
2174
+ terminal.start += terminal.buffer.length + terminal.pending.length + 1;
2175
+ terminal.buffer = "";
2176
+ terminal.pending = "";
2177
+ terminal.titleCarry = "";
2178
+ terminal.title = undefined;
2179
+ terminal.exit = undefined;
2180
+ terminal.cols = size.cols;
2181
+ terminal.rows = size.rows;
2182
+ for (const viewer of terminal.viewers)
2183
+ viewer.position = undefined;
2184
+ try {
2185
+ this.#start(terminal);
2186
+ } catch (error) {
2187
+ terminal.exit = { code: null };
2188
+ this.#changed(terminal.owner);
2189
+ throw error;
2190
+ }
2191
+ this.#changed(terminal.owner);
2192
+ }
2193
+ close(id) {
2194
+ const terminal = this.#get(id);
2195
+ this.#remove(terminal);
2196
+ this.#changed(terminal.owner);
2197
+ }
2198
+ closeOwners(owners) {
2199
+ for (const owner of new Set(owners)) {
2200
+ const owned = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner);
2201
+ if (owned.length === 0)
2202
+ continue;
2203
+ for (const terminal of owned)
2204
+ this.#remove(terminal);
2205
+ this.#changed(owner);
2206
+ }
2207
+ }
2208
+ closeAll() {
2209
+ this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
2210
+ }
2211
+ attach(id, since, sink) {
2212
+ const terminal = this.#get(id);
2213
+ const end = terminal.start + terminal.buffer.length;
2214
+ const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
2215
+ const viewer = { sink, position, sending: false };
2216
+ terminal.viewers.add(viewer);
2217
+ this.#pump(terminal, viewer);
2218
+ return () => {
2219
+ terminal.viewers.delete(viewer);
2220
+ };
2221
+ }
2222
+ #get(id) {
2223
+ const terminal = this.#terminals.get(id);
2224
+ if (terminal === undefined)
2225
+ throw new Error("Unknown terminal");
2226
+ return terminal;
2227
+ }
2228
+ #start(terminal) {
2229
+ const generation = ++terminal.generation;
2230
+ const { shell, env } = this.#options;
2231
+ const pty = this.#options.spawn(shell.file, [...shell.args], {
2232
+ name: "xterm-256color",
2233
+ cols: terminal.cols,
2234
+ rows: terminal.rows,
2235
+ cwd: terminal.cwd,
2236
+ env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
2237
+ });
2238
+ terminal.pty = pty;
2239
+ pty.onData((data) => {
2240
+ if (terminal.generation !== generation)
2241
+ return;
2242
+ terminal.pending += data;
2243
+ if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
2244
+ this.#flush(terminal);
2245
+ else
2246
+ terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
2247
+ });
2248
+ pty.onExit(({ exitCode, signal }) => {
2249
+ if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
2250
+ return;
2251
+ this.#flush(terminal);
2252
+ terminal.pty = undefined;
2253
+ terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
2254
+ this.#changed(terminal.owner);
2255
+ });
2256
+ }
2257
+ #stop(terminal) {
2258
+ if (terminal.flushTimer !== undefined)
2259
+ clearTimeout(terminal.flushTimer);
2260
+ terminal.flushTimer = undefined;
2261
+ terminal.generation++;
2262
+ const pty = terminal.pty;
2263
+ terminal.pty = undefined;
2264
+ try {
2265
+ pty?.kill();
2266
+ } catch {}
2267
+ }
2268
+ #remove(terminal) {
2269
+ this.#stop(terminal);
2270
+ this.#terminals.delete(terminal.id);
2271
+ for (const viewer of terminal.viewers)
2272
+ viewer.sink.ended("Terminal closed");
2273
+ terminal.viewers.clear();
2274
+ }
2275
+ #resize(terminal, size) {
2276
+ if (terminal.cols === size.cols && terminal.rows === size.rows)
2277
+ return;
2278
+ terminal.cols = size.cols;
2279
+ terminal.rows = size.rows;
2280
+ try {
2281
+ terminal.pty?.resize(size.cols, size.rows);
2282
+ } catch {}
2283
+ this.#changed(terminal.owner);
2284
+ }
2285
+ #flush(terminal) {
2286
+ if (terminal.flushTimer !== undefined)
2287
+ clearTimeout(terminal.flushTimer);
2288
+ terminal.flushTimer = undefined;
2289
+ const data = terminal.pending;
2290
+ if (data === "")
2291
+ return;
2292
+ terminal.pending = "";
2293
+ terminal.buffer += data;
2294
+ const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
2295
+ if (terminal.buffer.length > bufferSize) {
2296
+ let cut = terminal.buffer.length - bufferSize;
2297
+ const line = terminal.buffer.indexOf(`
2298
+ `, cut);
2299
+ if (line !== -1 && line - cut < 1024)
2300
+ cut = line + 1;
2301
+ terminal.buffer = terminal.buffer.slice(cut);
2302
+ terminal.start += cut;
2303
+ }
2304
+ if (this.#readTitle(terminal, data))
2305
+ this.#changed(terminal.owner);
2306
+ for (const viewer of terminal.viewers)
2307
+ this.#pump(terminal, viewer);
2308
+ }
2309
+ #readTitle(terminal, data) {
2310
+ const text = terminal.titleCarry + data;
2311
+ let title;
2312
+ let consumed = 0;
2313
+ for (const match of text.matchAll(TITLE)) {
2314
+ title = match[1];
2315
+ consumed = match.index + match[0].length;
2316
+ }
2317
+ const open = text.lastIndexOf("\x1B]");
2318
+ terminal.titleCarry = open >= consumed && text.length - open <= MAX_TITLE ? text.slice(open) : "";
2319
+ if (title === undefined || title === terminal.title)
2320
+ return false;
2321
+ terminal.title = title;
2322
+ return terminal.rename === undefined;
2323
+ }
2324
+ async#pump(terminal, viewer) {
2325
+ if (viewer.sending)
2326
+ return;
2327
+ viewer.sending = true;
2328
+ try {
2329
+ const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
2330
+ while (terminal.viewers.has(viewer)) {
2331
+ const end = terminal.start + terminal.buffer.length;
2332
+ const reset = viewer.position === undefined || viewer.position < terminal.start;
2333
+ const from = reset ? terminal.start : viewer.position;
2334
+ if (!reset && from >= end)
2335
+ return;
2336
+ let to = reset ? end : Math.min(end, from + maxChunk);
2337
+ if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
2338
+ to--;
2339
+ const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
2340
+ viewer.position = to;
2341
+ await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
2342
+ }
2343
+ } finally {
2344
+ viewer.sending = false;
2345
+ }
2346
+ }
2347
+ #changed(owner) {
2348
+ const watchers = this.#watchers.get(owner);
2349
+ if (watchers === undefined)
2350
+ return;
2351
+ const list = this.list(owner);
2352
+ for (const watcher of watchers)
2353
+ watcher(list);
2354
+ }
2355
+ }
2356
+ async function startTerminals(options = {}) {
2357
+ let spawn3 = options.spawn;
2358
+ if (spawn3 === undefined) {
2359
+ try {
2360
+ const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
2361
+ if (typeof loaded.spawn !== "function")
2362
+ throw new Error("the module has no spawn()");
2363
+ spawn3 = loaded.spawn;
2364
+ } catch (error) {
2365
+ const detail = error instanceof Error ? error.message : String(error);
2366
+ (options.diagnostic ?? console.error)(`Terminals unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.terminals. (${detail})`);
2367
+ return;
2368
+ }
2369
+ }
2370
+ return new Terminals({ ...options, spawn: spawn3, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
2371
+ }
2372
+ function loginShell() {
2373
+ let file = process.env.SHELL;
2374
+ if (file === undefined || file === "") {
2375
+ try {
2376
+ file = userInfo().shell ?? undefined;
2377
+ } catch {
2378
+ file = undefined;
2379
+ }
2380
+ }
2381
+ return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
2382
+ }
2383
+ function shellEnv(env) {
2384
+ const result2 = {};
2385
+ for (const [key, value] of Object.entries(env)) {
2386
+ if (value !== undefined && !key.startsWith("PINOMAD_"))
2387
+ result2[key] = value;
2388
+ }
2389
+ return result2;
2390
+ }
2391
+ function info(terminal) {
2392
+ return {
2393
+ id: terminal.id,
2394
+ name: terminal.rename ?? terminal.title ?? `Terminal ${terminal.number}`,
2395
+ renamed: terminal.rename !== undefined,
2396
+ cols: terminal.cols,
2397
+ rows: terminal.rows,
2398
+ ...terminal.exit === undefined ? {} : { exit: terminal.exit }
2399
+ };
2400
+ }
2401
+ function isHighSurrogate(code) {
2402
+ return code >= 55296 && code <= 56319;
2403
+ }
2404
+
2059
2405
  // apps/host/src/folders.ts
2060
2406
  import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
2061
2407
  import { isAbsolute, join as join6 } from "node:path";
@@ -3186,24 +3532,29 @@ async function attachPushSender(options) {
3186
3532
  }
3187
3533
  };
3188
3534
  const unsubscribeConversations = conversations.subscribe(() => update());
3189
- const unsubscribeCommits = harness.subscribeCommits((publication) => {
3190
- for (const change of publication.changes) {
3191
- if (change.type !== "document" || change.record.kind !== TitleDoc.definition.kind)
3192
- continue;
3193
- const id = change.conversationId;
3194
- if (id === undefined)
3195
- continue;
3196
- const title = change.value?.title;
3197
- if (typeof title !== "string" || title === "")
3198
- continue;
3199
- const waiters = titleWaiters.get(id);
3200
- titleWaiters.delete(id);
3201
- for (const settle of waiters ?? [])
3202
- settle(title);
3203
- }
3535
+ const commits = harness.observeCommits({
3536
+ advance: (publication) => {
3537
+ for (const change of publication.changes) {
3538
+ if (change.type !== "document" || change.kind !== "change" || change.record.kind !== TitleDoc.definition.kind)
3539
+ continue;
3540
+ const id = change.conversationId;
3541
+ if (id === undefined)
3542
+ continue;
3543
+ const title = change.value.title;
3544
+ if (typeof title !== "string" || title === "")
3545
+ continue;
3546
+ const waiters = titleWaiters.get(id);
3547
+ titleWaiters.delete(id);
3548
+ for (const settle of waiters ?? [])
3549
+ settle(title);
3550
+ }
3551
+ },
3552
+ end: () => {}
3204
3553
  });
3205
- indexWatch?.start(async (value) => {
3206
- index = value ?? undefined;
3554
+ indexWatch?.start(async (call) => {
3555
+ if (call.kind !== "value")
3556
+ return;
3557
+ index = call.value;
3207
3558
  update();
3208
3559
  });
3209
3560
  update();
@@ -3212,7 +3563,7 @@ async function attachPushSender(options) {
3212
3563
  async close() {
3213
3564
  disposed = true;
3214
3565
  unsubscribeConversations();
3215
- unsubscribeCommits();
3566
+ commits.detach();
3216
3567
  await indexWatch?.stop();
3217
3568
  for (const item of pending)
3218
3569
  drop(item);
@@ -3351,6 +3702,7 @@ async function startGateway(options) {
3351
3702
  const clients = new Set;
3352
3703
  let rtc;
3353
3704
  const rtcHello = () => rtc?.hello();
3705
+ const terminals = await startTerminals(options.terminals);
3354
3706
  const broadcast = (level, message) => {
3355
3707
  for (const client of clients)
3356
3708
  client.send({ type: "notice", level, message });
@@ -3365,10 +3717,12 @@ async function startGateway(options) {
3365
3717
  }
3366
3718
  rtc?.closeDevice(publicKey);
3367
3719
  };
3720
+ const reachableRelay = () => relay?.state().status === "removed" ? undefined : relay;
3368
3721
  const currentAdvertisement = () => {
3369
3722
  const advertiseUrl = remote?.advertiseUrl;
3723
+ const relay2 = reachableRelay();
3370
3724
  const endpoints = remoteEndpointCandidates({
3371
- ...relay === undefined ? {} : { relay: { origin: relay.origin, hostId: relay.hostId } },
3725
+ ...relay2 === undefined ? {} : { relay: { origin: relay2.origin, hostId: relay2.hostId } },
3372
3726
  ...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
3373
3727
  });
3374
3728
  return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
@@ -3385,6 +3739,7 @@ async function startGateway(options) {
3385
3739
  const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
3386
3740
  devicePublicKey,
3387
3741
  rtcHello,
3742
+ terminals,
3388
3743
  closed: (self) => {
3389
3744
  clients.delete(self);
3390
3745
  connections.delete(self);
@@ -3405,11 +3760,12 @@ async function startGateway(options) {
3405
3760
  const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
3406
3761
  const advertisement = currentAdvertisement();
3407
3762
  const endpointSet = checkedEndpointSet(advertisement.endpoints);
3763
+ const relay2 = reachableRelay();
3408
3764
  const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
3409
- if (relayReachable && relay !== undefined) {
3765
+ if (relayReachable && relay2 !== undefined) {
3410
3766
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3411
3767
  return {
3412
- url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId), endpointSet)}`,
3768
+ url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId), endpointSet)}`,
3413
3769
  expiresAt: expiresAt2
3414
3770
  };
3415
3771
  }
@@ -3422,10 +3778,10 @@ async function startGateway(options) {
3422
3778
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3423
3779
  return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
3424
3780
  }
3425
- if (relay !== undefined) {
3781
+ if (relay2 !== undefined) {
3426
3782
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3427
3783
  return {
3428
- url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId))}`,
3784
+ url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId))}`,
3429
3785
  expiresAt: expiresAt2
3430
3786
  };
3431
3787
  }
@@ -3519,6 +3875,10 @@ async function startGateway(options) {
3519
3875
  });
3520
3876
  if (secure?.relay !== undefined) {
3521
3877
  const relayOrigin = secure.relay.origin;
3878
+ const startConfig = await readServerConfig(options.dataDir).catch(() => {
3879
+ return;
3880
+ });
3881
+ const linkJoin = startConfig !== undefined && normalizeOrigin(startConfig.origin) === normalizeOrigin(relayOrigin) ? startConfig : undefined;
3522
3882
  const currentBinding = async () => {
3523
3883
  const config = await readServerConfig(options.dataDir).catch(() => {
3524
3884
  return;
@@ -3572,6 +3932,16 @@ async function startGateway(options) {
3572
3932
  console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
3573
3933
  } else if (state.status === "connecting") {
3574
3934
  console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
3935
+ } else if (state.status === "removed") {
3936
+ broadcast("warning", state.reason);
3937
+ console.error(state.reason);
3938
+ markRemoved(options.dataDir, {
3939
+ origin: relayOrigin,
3940
+ ...linkJoin?.binding === undefined ? {} : { binding: linkJoin.binding },
3941
+ ...linkJoin?.username === undefined ? {} : { username: linkJoin.username }
3942
+ }).catch((error) => {
3943
+ console.error(`could not write ${REMOVAL_NOTICE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
3944
+ });
3575
3945
  } else {
3576
3946
  broadcast("warning", state.reason);
3577
3947
  console.error(state.reason);
@@ -3606,6 +3976,7 @@ async function startGateway(options) {
3606
3976
  detachSettingsWatch();
3607
3977
  await detachPush?.close();
3608
3978
  conversations.dispose();
3979
+ terminals?.closeAll();
3609
3980
  await rtc?.close();
3610
3981
  await Promise.all([...connections].map((connection) => connection.dispose()));
3611
3982
  for (const socket of server.clients)
@@ -3908,12 +4279,16 @@ class ConversationList {
3908
4279
  #list = [];
3909
4280
  #unsubscribe = () => {};
3910
4281
  #notifying = false;
4282
+ #harness;
4283
+ #live = new Map;
4284
+ #disposed = false;
3911
4285
  constructor(docs) {
3912
4286
  this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
3913
4287
  this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
3914
4288
  }
3915
4289
  static async open(harness, docs) {
3916
4290
  const list = new ConversationList(docs);
4291
+ list.#harness = harness;
3917
4292
  const summaries = [];
3918
4293
  let cursor;
3919
4294
  const records = [];
@@ -3932,10 +4307,13 @@ class ConversationList {
3932
4307
  summaries.push(list.#summaryOf(record));
3933
4308
  }
3934
4309
  list.#list = summaries;
3935
- list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
4310
+ const observation = harness.observeCommits({ advance: (publication) => list.#observe(publication), end: () => {} });
4311
+ list.#unsubscribe = () => observation.detach();
3936
4312
  const touched = new Set;
3937
4313
  list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
3938
4314
  list.#refreshSummaries(touched);
4315
+ for (const [id, facts] of list.#facts)
4316
+ list.#followLive(id, facts.running === true);
3939
4317
  return list;
3940
4318
  }
3941
4319
  async#load(harness, record) {
@@ -3956,6 +4334,7 @@ class ConversationList {
3956
4334
  facts.label = subagent.label;
3957
4335
  if (typeof subagent?.role === "string" && subagent.role !== "")
3958
4336
  facts.role = subagent.role;
4337
+ facts.model = modelRefOf(subagent?.model);
3959
4338
  }
3960
4339
  facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
3961
4340
  const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
@@ -3990,22 +4369,23 @@ class ConversationList {
3990
4369
  facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
3991
4370
  } else if (change.type === "document") {
3992
4371
  const id = change.conversationId;
4372
+ const value = change.kind === "change" ? change.value : null;
3993
4373
  if (id === undefined) {
3994
4374
  if (change.record.kind === IndexDoc.definition.kind)
3995
- this.#reindex(change.value, touched);
4375
+ this.#reindex(value, touched);
3996
4376
  continue;
3997
4377
  }
3998
4378
  const facts = this.#fact(id);
3999
4379
  if (change.record.kind === "pi.live") {
4000
- const live = change.value;
4380
+ const live = value;
4001
4381
  const wasRunning = facts.running === true;
4002
4382
  facts.running = live?.run !== undefined;
4003
4383
  facts.tools = live?.tools;
4004
4384
  touched.add(id);
4385
+ this.#followLive(id, facts.running);
4005
4386
  if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
4006
4387
  ended.add(id);
4007
4388
  } else if (this.#questionKinds.has(change.record.kind)) {
4008
- const value = change.value;
4009
4389
  const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
4010
4390
  const next = new Set(facts.pending);
4011
4391
  if (pending)
@@ -4015,15 +4395,16 @@ class ConversationList {
4015
4395
  facts.pending = next;
4016
4396
  touched.add(id);
4017
4397
  } else if (change.record.kind === RoleDoc.definition.kind) {
4018
- facts.agent = roleNameOf(change.value);
4398
+ facts.agent = roleNameOf(value);
4019
4399
  touched.add(id);
4020
4400
  } else if (change.record.kind === SubagentDoc.definition.kind) {
4021
- const value = change.value;
4022
- facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
4023
- facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
4401
+ const subagent = value;
4402
+ facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
4403
+ facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
4404
+ facts.model = modelRefOf(subagent?.model);
4024
4405
  touched.add(id);
4025
4406
  } else if (change.record.kind === TitleDoc.definition.kind) {
4026
- const named = change.value?.title;
4407
+ const named = value?.title;
4027
4408
  facts.named = typeof named === "string" && named !== "" ? named : undefined;
4028
4409
  touched.add(id);
4029
4410
  }
@@ -4098,9 +4479,51 @@ class ConversationList {
4098
4479
  listener(this.#list);
4099
4480
  });
4100
4481
  }
4482
+ #followLive(id, running) {
4483
+ if (!running || this.#disposed) {
4484
+ const watch = this.#live.get(id);
4485
+ this.#live.delete(id);
4486
+ watch?.stop();
4487
+ return;
4488
+ }
4489
+ if (this.#live.has(id))
4490
+ return;
4491
+ this.#live.set(id, undefined);
4492
+ setImmediate(() => {
4493
+ this.#harness.watchDoc(LiveDoc, id, context3).then(async (watch) => {
4494
+ if (watch === undefined)
4495
+ return;
4496
+ if (this.#live.get(id) !== undefined || !this.#live.has(id)) {
4497
+ await watch.stop();
4498
+ return;
4499
+ }
4500
+ this.#live.set(id, watch);
4501
+ const apply = (live) => {
4502
+ const facts = this.#fact(id);
4503
+ if (live.run === undefined)
4504
+ return;
4505
+ facts.tools = live.tools;
4506
+ this.#refreshSummaries(new Set([id]));
4507
+ };
4508
+ apply(watch.value);
4509
+ watch.start(async (call) => {
4510
+ if (call.kind === "value")
4511
+ apply(call.value);
4512
+ });
4513
+ }, (error) => {
4514
+ if (this.#live.get(id) === undefined)
4515
+ this.#live.delete(id);
4516
+ console.error(`conversation ${id}: could not follow pi.live: ${error instanceof Error ? error.message : String(error)}`);
4517
+ });
4518
+ });
4519
+ }
4101
4520
  dispose() {
4521
+ this.#disposed = true;
4102
4522
  this.#unsubscribe();
4103
4523
  this.#listeners.clear();
4524
+ for (const watch of this.#live.values())
4525
+ watch?.stop();
4526
+ this.#live.clear();
4104
4527
  }
4105
4528
  }
4106
4529
  function roleNameOf(value) {
@@ -4108,6 +4531,14 @@ function roleNameOf(value) {
4108
4531
  return;
4109
4532
  return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
4110
4533
  }
4534
+ function modelRefOf(value) {
4535
+ if (typeof value !== "string")
4536
+ return;
4537
+ const slash = value.indexOf("/");
4538
+ if (slash <= 0 || slash === value.length - 1)
4539
+ return;
4540
+ return { provider: value.slice(0, slash), modelId: value.slice(slash + 1) };
4541
+ }
4111
4542
  function rolesOf(index) {
4112
4543
  const roles = new Map;
4113
4544
  for (const entry of index?.conversations ?? []) {
@@ -4138,6 +4569,8 @@ function summaryOf(record, facts, role, currentAgent) {
4138
4569
  return {
4139
4570
  id: record.id,
4140
4571
  kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
4572
+ ...record.owner === undefined ? {} : { lifecycle: "foreground" },
4573
+ ...facts.model === undefined ? {} : { model: facts.model },
4141
4574
  ...parent === undefined ? {} : { parent },
4142
4575
  ...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
4143
4576
  ...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
@@ -4150,7 +4583,7 @@ function summaryOf(record, facts, role, currentAgent) {
4150
4583
  };
4151
4584
  }
4152
4585
  function sameSummary(a, b) {
4153
- return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
4586
+ return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
4154
4587
  }
4155
4588
  function titleOf(entry) {
4156
4589
  const message = entry?.model?.[0];
@@ -4159,6 +4592,38 @@ function titleOf(entry) {
4159
4592
  const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
4160
4593
  return title === "" ? {} : { title };
4161
4594
  }
4595
+ var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
4596
+ var TURN_STARTS = new Set(["pi.user", "pi.compaction", "pi.reset"]);
4597
+ async function turnPage(conversation, before, limit) {
4598
+ const want = Math.min(limit, MAX_TRANSCRIPT_PAGE);
4599
+ const done = (taken2) => taken2.length >= MAX_TRANSCRIPT_PAGE || taken2.length >= want && TURN_STARTS.has(taken2.at(-1).kind);
4600
+ const query = { order: "descending", ...before === undefined ? {} : { maxEntryId: before } };
4601
+ const taken = [];
4602
+ let cursor;
4603
+ for (;; ) {
4604
+ const page = await conversation.entries(query, Math.max(want + 1 - taken.length, 64), cursor, context3);
4605
+ for (const entry of page.items) {
4606
+ if (done(taken))
4607
+ return { entries: taken.reverse(), before: entry.id };
4608
+ taken.push(entry);
4609
+ }
4610
+ if (page.next === undefined)
4611
+ return { entries: taken.reverse() };
4612
+ cursor = page.next;
4613
+ }
4614
+ }
4615
+ async function viewWindow(conversation, window) {
4616
+ if (window === undefined)
4617
+ return WHOLE_TRANSCRIPT;
4618
+ const from = window.from ?? await startOf(conversation, window.limit);
4619
+ return from === undefined ? WHOLE_TRANSCRIPT : { ...WHOLE_TRANSCRIPT, from };
4620
+ }
4621
+ async function startOf(conversation, limit) {
4622
+ const page = await turnPage(conversation, undefined, limit);
4623
+ return page.before === undefined ? undefined : page.entries[0].id;
4624
+ }
4625
+ var shownOps = (call) => isGone(call) ? [["r", null]] : call.ops;
4626
+ var isGone = (call) => call.retired && !call.ops.some((op) => op[0] === "r");
4162
4627
  async function historyFacts(harness, id) {
4163
4628
  const conversation = await harness.conversation(id, context3);
4164
4629
  let first;
@@ -4293,6 +4758,7 @@ class GatewayClient {
4293
4758
  }
4294
4759
  #hostValues;
4295
4760
  #subscriptions = new Map;
4761
+ #terminalSizes = new Map;
4296
4762
  #disposed = false;
4297
4763
  constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
4298
4764
  this.#connection = connection;
@@ -4355,7 +4821,8 @@ class GatewayClient {
4355
4821
  })),
4356
4822
  toolPresentations: options.toolPresentations ?? {},
4357
4823
  endpointSet: this.#hooks.endpointSet(),
4358
- ...rtc === undefined ? {} : { rtc }
4824
+ ...rtc === undefined ? {} : { rtc },
4825
+ ...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
4359
4826
  });
4360
4827
  }
4361
4828
  send(frame) {
@@ -4368,15 +4835,19 @@ class GatewayClient {
4368
4835
  }
4369
4836
  async#receive(frame) {
4370
4837
  if (frame.type === "subscribe")
4371
- await this.#subscribe(frame.stream, frame.images === "ref");
4838
+ await this.#subscribe(frame.stream, frame.images === "ref", frame.since, frame.window);
4372
4839
  else if (frame.type === "unsubscribe")
4373
4840
  await this.#unsubscribe(frame.stream);
4374
4841
  else
4375
4842
  await this.#answer(frame.id, frame.method, frame.args);
4376
4843
  }
4377
- async#subscribe(stream, imageRefs) {
4844
+ async#subscribe(stream, imageRefs, since, window) {
4378
4845
  const previous = this.#subscriptions.get(stream);
4379
- const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
4846
+ const state = {
4847
+ ...imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {},
4848
+ ...since === undefined ? {} : { since },
4849
+ ...window === undefined ? {} : { window }
4850
+ };
4380
4851
  this.#subscriptions.set(stream, state);
4381
4852
  const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
4382
4853
  const send = (frame) => current() ? this.send(frame) : Promise.resolve();
@@ -4420,12 +4891,30 @@ class GatewayClient {
4420
4891
  }
4421
4892
  if (stream === "tasks")
4422
4893
  return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
4894
+ if (stream.startsWith("terminals:")) {
4895
+ const terminals = this.#terminals();
4896
+ const conversation2 = await this.#conversation(Number(stream.slice("terminals:".length)));
4897
+ const { owner } = await this.#terminalOwner(conversation2);
4898
+ return { stop: terminals.watch(owner, (value) => void send({ type: "snapshot", stream, value })) };
4899
+ }
4900
+ if (stream.startsWith("terminal:")) {
4901
+ const terminalStream = stream;
4902
+ const detach = this.#terminals().attach(terminalStream.slice("terminal:".length), state?.since, {
4903
+ output: (frame) => send({ type: "output", stream: terminalStream, ...frame }),
4904
+ ended: (reason) => {
4905
+ send({ type: "ended", stream, reason });
4906
+ if (this.#subscriptions.get(stream) === state)
4907
+ this.#subscriptions.delete(stream);
4908
+ }
4909
+ });
4910
+ return { stop: detach };
4911
+ }
4423
4912
  if (stream.startsWith("doc:")) {
4424
4913
  const at = stream.lastIndexOf(":");
4425
4914
  return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
4426
4915
  }
4427
4916
  const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
4428
- return this.#forward(stream, await conversation.watch(context3), send, state);
4917
+ return this.#forward(stream, await conversation.watch(context3, await viewWindow(conversation, state?.window)), send, state);
4429
4918
  }
4430
4919
  #hostValue(stream) {
4431
4920
  return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
@@ -4443,13 +4932,17 @@ class GatewayClient {
4443
4932
  });
4444
4933
  let inner;
4445
4934
  let stopped = false;
4446
- const unsubscribe = harness.subscribeCommits((publication) => {
4447
- const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
4448
- if (!matches)
4449
- return;
4450
- unsubscribe();
4451
- setImmediate(() => signalCreation());
4935
+ const observation = harness.observeCommits({
4936
+ advance: (publication) => {
4937
+ const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
4938
+ if (!matches)
4939
+ return;
4940
+ unsubscribe();
4941
+ setImmediate(() => signalCreation());
4942
+ },
4943
+ end: () => {}
4452
4944
  });
4945
+ const unsubscribe = () => observation.detach();
4453
4946
  try {
4454
4947
  const existing = await harness.watchDoc(token, id, context3);
4455
4948
  if (existing !== undefined) {
@@ -4490,16 +4983,19 @@ class GatewayClient {
4490
4983
  async#forward(stream, watch, send, state) {
4491
4984
  if (state?.images === undefined) {
4492
4985
  await send({ type: "snapshot", stream, value: watch.value });
4493
- watch.start((_value, ops) => send({ type: "ops", stream, ops }));
4986
+ watch.start((call) => call.kind === "value" ? send({ type: "ops", stream, ops: shownOps(call) }) : Promise.resolve());
4494
4987
  return watch;
4495
4988
  }
4496
4989
  const images = state.images;
4497
4990
  let before = watch.value;
4498
4991
  state.view = before;
4499
4992
  await send({ type: "snapshot", stream, value: await images.value(before) });
4500
- watch.start(async (value, ops) => {
4993
+ watch.start(async (call) => {
4994
+ if (call.kind !== "value")
4995
+ return;
4996
+ const ops = shownOps(call);
4501
4997
  const sent = await images.ops(ops, before);
4502
- before = value;
4998
+ before = isGone(call) ? null : call.value;
4503
4999
  state.view = before;
4504
5000
  return send({ type: "ops", stream, ops: sent });
4505
5001
  });
@@ -4509,7 +5005,7 @@ class GatewayClient {
4509
5005
  for (const state of this.#subscriptions.values()) {
4510
5006
  if (state.images === undefined || state.view === undefined)
4511
5007
  continue;
4512
- const image = state.images.image(state.view, ref);
5008
+ const image = state.images.image(state.view, ref) ?? (state.earlier === undefined ? undefined : state.images.image({ entries: state.earlier }, ref));
4513
5009
  if (image !== undefined)
4514
5010
  return image;
4515
5011
  }
@@ -4569,10 +5065,43 @@ class GatewayClient {
4569
5065
  await this.send({ type: "result", id, ok: true, value: value ?? null });
4570
5066
  } catch (error) {
4571
5067
  await this.send({ type: "result", id, ok: false, error: error instanceof Error ? error.message : String(error) });
5068
+ return;
5069
+ }
5070
+ const device = this.devicePublicKey;
5071
+ if (method === "revokeDevice" && device !== undefined && args.publicKey === device) {
5072
+ this.#hooks.closeDevice(device);
4572
5073
  }
4573
5074
  }
4574
5075
  async#call(method, args) {
4575
5076
  switch (method) {
5077
+ case "terminalInput": {
5078
+ const input = args;
5079
+ this.#terminals().input(input.terminalId, input.data, this.#terminalSizes.get(input.terminalId));
5080
+ return null;
5081
+ }
5082
+ case "resizeTerminal": {
5083
+ const input = args;
5084
+ const size = { cols: input.cols, rows: input.rows };
5085
+ this.#terminals().resize(input.terminalId, size);
5086
+ this.#terminalSizes.set(input.terminalId, size);
5087
+ return null;
5088
+ }
5089
+ case "renameTerminal": {
5090
+ const input = args;
5091
+ this.#terminals().rename(input.terminalId, input.name);
5092
+ return null;
5093
+ }
5094
+ case "restartTerminal": {
5095
+ const input = args;
5096
+ const size = { cols: input.cols, rows: input.rows };
5097
+ this.#terminals().restart(input.terminalId, size);
5098
+ this.#terminalSizes.set(input.terminalId, size);
5099
+ return null;
5100
+ }
5101
+ case "closeTerminal":
5102
+ this.#terminals().close(args.terminalId);
5103
+ this.#terminalSizes.delete(args.terminalId);
5104
+ return null;
4576
5105
  case "addProject":
4577
5106
  return addProject(this.#options.harness, args.path, context3);
4578
5107
  case "removeProject":
@@ -4636,6 +5165,7 @@ class GatewayClient {
4636
5165
  const input = args;
4637
5166
  await archive(this.#options.harness, input.conversationId, input.archived, context3);
4638
5167
  if (input.archived) {
5168
+ this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
4639
5169
  const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
4640
5170
  for (const { record, reason } of kept) {
4641
5171
  await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
@@ -4652,7 +5182,8 @@ class GatewayClient {
4652
5182
  case "revokeDevice": {
4653
5183
  const { publicKey } = args;
4654
5184
  await revokeDevice(this.#options.harness, publicKey, context3);
4655
- this.#hooks.closeDevice(publicKey);
5185
+ if (publicKey !== this.devicePublicKey)
5186
+ this.#hooks.closeDevice(publicKey);
4656
5187
  return null;
4657
5188
  }
4658
5189
  case "rtcOffer": {
@@ -4738,6 +5269,26 @@ class GatewayClient {
4738
5269
  const input = args;
4739
5270
  return listSkills(this.#options.harness, this.#skills, input.scope, context3);
4740
5271
  }
5272
+ case "earlierEntries": {
5273
+ const { conversationId, before, limit } = args;
5274
+ const stream = `conversation:${conversationId}`;
5275
+ const state = this.#subscriptions.get(stream);
5276
+ if (state === undefined)
5277
+ throw new Error(`Not subscribed to conversation ${conversationId}`);
5278
+ const replaced = () => this.#subscriptions.get(stream) !== state;
5279
+ const resubscribed = () => new Error(`Conversation ${conversationId} was resubscribed while its page loaded`);
5280
+ const page = await turnPage(await this.#conversation(conversationId), before, limit);
5281
+ if (replaced())
5282
+ throw resubscribed();
5283
+ if (state.images === undefined)
5284
+ return page;
5285
+ const raw = page.entries;
5286
+ const { entries } = await state.images.value({ entries: raw });
5287
+ if (replaced())
5288
+ throw resubscribed();
5289
+ state.earlier = [...state.earlier ?? [], ...raw];
5290
+ return { ...page, entries };
5291
+ }
4741
5292
  case "image": {
4742
5293
  const input = args;
4743
5294
  const image = this.#shownImage(input.ref);
@@ -4799,6 +5350,37 @@ class GatewayClient {
4799
5350
  return { outcome: "steered" };
4800
5351
  }, context3);
4801
5352
  }
5353
+ case "reorderQueued": {
5354
+ const { conversationId, submissionId, before } = args;
5355
+ return await this.#options.harness.commit(async (tx) => {
5356
+ const queuedState = async (id) => {
5357
+ const record = await tx.submission(id);
5358
+ if (record === undefined)
5359
+ throw new Error(`Submission ${id} does not exist`);
5360
+ assertQueuedInput(record, conversationId);
5361
+ if (record.entry !== undefined)
5362
+ return "sent";
5363
+ return record.status === "queued" ? "queued" : "unavailable";
5364
+ };
5365
+ const moved = await queuedState(submissionId);
5366
+ if (moved !== "queued")
5367
+ return { outcome: moved };
5368
+ if (before !== undefined && await queuedState(before) !== "queued") {
5369
+ return { outcome: "unavailable" };
5370
+ }
5371
+ if (before === submissionId)
5372
+ return { outcome: "moved" };
5373
+ const items = (await tx.doc(InboxDoc, conversationId)).items;
5374
+ const from = items.findIndex((candidate) => candidate.id === submissionId);
5375
+ if (from < 0 || before !== undefined && !items.some((candidate) => candidate.id === before)) {
5376
+ return { outcome: "unavailable" };
5377
+ }
5378
+ const [item] = items.splice(from, 1);
5379
+ const to = before === undefined ? items.findLastIndex((candidate) => candidate.mode !== "write") + 1 : items.findIndex((candidate) => candidate.id === before);
5380
+ items.splice(to, 0, item);
5381
+ return { outcome: "moved" };
5382
+ }, context3);
5383
+ }
4802
5384
  case "renameConversation": {
4803
5385
  const { title } = args;
4804
5386
  const trimmed = title.trim();
@@ -4935,8 +5517,34 @@ class GatewayClient {
4935
5517
  case "changes": {
4936
5518
  return await this.#changes(conversation, context3);
4937
5519
  }
5520
+ case "openTerminal": {
5521
+ const input = args;
5522
+ const terminals = this.#terminals();
5523
+ const { owner, cwd, record } = await this.#terminalOwner(conversation);
5524
+ const ready = record === undefined || await worktreeSettled(record);
5525
+ if (!ready || !await stat6(cwd).then((info2) => info2.isDirectory(), () => false)) {
5526
+ throw new Error(`The checkout is unavailable: ${cwd}`);
5527
+ }
5528
+ const size = { cols: input.cols, rows: input.rows };
5529
+ const terminalId = terminals.open(owner, cwd, size);
5530
+ this.#terminalSizes.set(terminalId, size);
5531
+ return { terminalId };
5532
+ }
4938
5533
  }
4939
5534
  }
5535
+ #terminals() {
5536
+ if (this.#hooks.terminals === undefined)
5537
+ throw new Error("Terminals are unavailable on this host");
5538
+ return this.#hooks.terminals;
5539
+ }
5540
+ async#terminalOwner(conversation) {
5541
+ const cwd = (await conversation.agent(context3)).cwd;
5542
+ if (cwd === undefined)
5543
+ throw new Error("No working directory");
5544
+ const record = checkoutAt((await this.#options.harness.snapshot(IndexDoc, context3))?.checkouts, cwd);
5545
+ const owner = record?.conversationId ?? await familyRoot(this.#options.harness, conversation.id, context3);
5546
+ return { owner, cwd, record };
5547
+ }
4940
5548
  async#expandSkills(text, scope, bound) {
4941
5549
  if (bound === undefined ? !text.includes("$") : bound.length === 0)
4942
5550
  return text;
@@ -5524,7 +6132,7 @@ import {
5524
6132
  readdir as readdir3,
5525
6133
  rename as rename2,
5526
6134
  rm as rm3,
5527
- stat as stat6,
6135
+ stat as stat7,
5528
6136
  writeFile as writeFile2
5529
6137
  } from "node:fs/promises";
5530
6138
  import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
@@ -5713,7 +6321,7 @@ class AttachmentStore {
5713
6321
  throw new Error(problem);
5714
6322
  const decoded = base64DecodedLength(data);
5715
6323
  const path = this.#dataPath(meta);
5716
- const received = (await stat6(path)).size;
6324
+ const received = (await stat7(path)).size;
5717
6325
  if (received === meta.size) {
5718
6326
  await chmod(path, 292);
5719
6327
  await this.#writeMeta({ ...meta, state: "complete" });
@@ -5756,13 +6364,13 @@ class AttachmentStore {
5756
6364
  if (meta.state === "uploading")
5757
6365
  throw new Error(`Attachment ${index + 1} is still uploading`);
5758
6366
  const path = this.#dataPath(meta);
5759
- let info;
6367
+ let info2;
5760
6368
  try {
5761
- info = await lstat(path);
6369
+ info2 = await lstat(path);
5762
6370
  } catch {
5763
6371
  throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
5764
6372
  }
5765
- if (!info.isFile() || info.size !== meta.size) {
6373
+ if (!info2.isFile() || info2.size !== meta.size) {
5766
6374
  throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
5767
6375
  }
5768
6376
  checked.push({ meta, path });
@@ -5805,7 +6413,7 @@ class AttachmentStore {
5805
6413
  } else {
5806
6414
  this.#pending.delete(entry.name);
5807
6415
  try {
5808
- if (now - (await stat6(directory)).mtimeMs <= this.#ttlMs)
6416
+ if (now - (await stat7(directory)).mtimeMs <= this.#ttlMs)
5809
6417
  continue;
5810
6418
  } catch {
5811
6419
  continue;
@@ -6004,6 +6612,7 @@ async function openHost(options) {
6004
6612
  }
6005
6613
  },
6006
6614
  ...options.rtc === undefined ? {} : { rtc: options.rtc },
6615
+ ...options.terminals === undefined ? {} : { terminals: options.terminals },
6007
6616
  secure: {
6008
6617
  hostKey,
6009
6618
  offers,
@@ -6085,7 +6694,7 @@ async function openHost(options) {
6085
6694
  }
6086
6695
 
6087
6696
  // apps/host/src/service/log-rotation.ts
6088
- import { copyFile, rename as rename3, rm as rm4, stat as stat7, truncate } from "node:fs/promises";
6697
+ import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
6089
6698
  import { join as join11 } from "node:path";
6090
6699
  var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
6091
6700
  var HOST_LOG_HISTORY = 3;
@@ -6099,7 +6708,7 @@ async function rotateHostLog(dataDir, copy = copyFile) {
6099
6708
  await rm4(temp, { force: true });
6100
6709
  let size;
6101
6710
  try {
6102
- size = (await stat7(log)).size;
6711
+ size = (await stat8(log)).size;
6103
6712
  } catch (error) {
6104
6713
  if (isMissing(error))
6105
6714
  return false;
@@ -6147,7 +6756,7 @@ function startHostLogRotation(dataDir) {
6147
6756
  }
6148
6757
 
6149
6758
  // apps/host/src/model-auth.ts
6150
- import { randomUUID as randomUUID2 } from "node:crypto";
6759
+ import { randomUUID as randomUUID3 } from "node:crypto";
6151
6760
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
6152
6761
  import { dirname as dirname3 } from "node:path";
6153
6762
  import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
@@ -6430,6 +7039,45 @@ async function openModelAuth(runtime, options) {
6430
7039
  tail = next.catch(() => {});
6431
7040
  return next;
6432
7041
  };
7042
+ const opened = await readModelsFile(options.modelsPath);
7043
+ Promise.all([...custom].flatMap(([id, endpoint]) => {
7044
+ const entries = opened.providers[id]?.models;
7045
+ if (!Array.isArray(entries) || !entries.some(lacksLimits))
7046
+ return [];
7047
+ return [
7048
+ (async () => {
7049
+ const key = (await runtime.getAuth(id))?.auth.apiKey;
7050
+ const listed = await fetchEndpointModels(endpoint.api, endpoint.baseUrl, key).catch(() => []);
7051
+ return { id, endpoint, listed: new Map(listed.map((model) => [model.id, model])) };
7052
+ })()
7053
+ ];
7054
+ })).then((answers) => serial(async () => {
7055
+ if (closed)
7056
+ return;
7057
+ const filled = new Set;
7058
+ const fill = (target) => {
7059
+ for (const { id, endpoint, listed } of answers) {
7060
+ const entry = target.providers[id];
7061
+ if (entry?.api !== endpoint.api || entry.baseUrl !== endpoint.baseUrl || !Array.isArray(entry.models))
7062
+ continue;
7063
+ for (const model of entry.models) {
7064
+ const limits = listed.get(model.id);
7065
+ for (const field of LIMITS) {
7066
+ if (field in model || limits?.[field] === undefined)
7067
+ continue;
7068
+ model[field] = limits[field];
7069
+ filled.add(id);
7070
+ }
7071
+ }
7072
+ }
7073
+ };
7074
+ fill(await readModelsFile(options.modelsPath));
7075
+ if (filled.size === 0)
7076
+ return;
7077
+ await updateModelsFile(fill);
7078
+ await runtime.refresh({ providers: [...filled], allowNetwork: false });
7079
+ await republish();
7080
+ }));
6433
7081
  return {
6434
7082
  models,
6435
7083
  auth,
@@ -6451,7 +7099,7 @@ async function openModelAuth(runtime, options) {
6451
7099
  if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
6452
7100
  throw new Error(`A login for ${provider.name} is already in progress`);
6453
7101
  }
6454
- const id = randomUUID2();
7102
+ const id = randomUUID3();
6455
7103
  const login = {
6456
7104
  flow: { id, providerId, method, events: [], state: "running" },
6457
7105
  controller: new AbortController,
@@ -6635,6 +7283,8 @@ async function fetchEndpointModels(api, baseUrl, key) {
6635
7283
  throw new Error(`${url.host} listed no models`);
6636
7284
  return models;
6637
7285
  }
7286
+ var LIMITS = ["contextWindow", "maxTokens"];
7287
+ var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
6638
7288
  var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
6639
7289
  function endpointModel(id, reported) {
6640
7290
  return {
@@ -6795,7 +7445,7 @@ async function main(argv) {
6795
7445
  process.on("SIGINT", stop);
6796
7446
  process.on("SIGTERM", stop);
6797
7447
  host.closed.then((end) => {
6798
- if (end.reason !== "failed" || stopping)
7448
+ if (end.kind !== "failed" || stopping)
6799
7449
  return;
6800
7450
  stopping = true;
6801
7451
  stopLogRotation();
@@ -6826,7 +7476,7 @@ async function installId(dataDir) {
6826
7476
  const path = join12(dataDir, "install-id");
6827
7477
  let id = (await readFile7(path, "utf8").catch(() => "")).trim();
6828
7478
  if (id === "") {
6829
- id = randomUUID3();
7479
+ id = randomUUID4();
6830
7480
  await mkdir6(dataDir, { recursive: true });
6831
7481
  await writeFile5(path, `${id}
6832
7482
  `, { mode: 384 });