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
package/README.md CHANGED
@@ -17,7 +17,7 @@ A self-hosted coding agent built on [Pi Durable](https://www.npmjs.com/package/@
17
17
  ## Quick start
18
18
 
19
19
  1. **Create an account** at [app.pinomad.si](https://app.pinomad.si/) (sign-up is by invite).
20
- 2. **Add your host.** On the machine that should run the agent (macOS or Linux), run the two lines below. `pinomad up` prints a code; enter it on the page it opens, signed in, to add the host to your account. It then runs as a service.
20
+ 2. **Add your host.** On the machine that should run the agent (macOS or Linux), run the two lines below. `pinomad up` opens a page in your browser; sign in and confirm to add the host to your account. It then runs as a service. Without a browser on that machine (over SSH, say), it prints a code to enter on any signed-in device instead.
21
21
 
22
22
  ```sh
23
23
  curl -fsSL https://pinomad.si/install.sh | sh
@@ -16,7 +16,7 @@ var NOT_BUILT = "Web client not built: run bun run build";
16
16
  var SECURITY_HEADERS = {
17
17
  "content-security-policy": [
18
18
  "default-src 'self'",
19
- "script-src 'self'",
19
+ "script-src 'self' 'wasm-unsafe-eval'",
20
20
  "style-src 'self' 'unsafe-inline'",
21
21
  "img-src 'self' data:",
22
22
  "connect-src 'self' ws: wss:",
@@ -3,7 +3,9 @@ import {
3
3
  MAX_ATTACHMENTS_PER_MESSAGE,
4
4
  PROTOCOL,
5
5
  RTC_DATA_CHANNEL_LABEL,
6
+ TRANSCRIPT_PAGE,
6
7
  UNAUTHORIZED_CLOSE_CODE,
8
+ addEarlier,
7
9
  checkoutOf,
8
10
  conversationStream,
9
11
  docStream,
@@ -11,11 +13,13 @@ import {
11
13
  hostStreams,
12
14
  hostSupportsCall,
13
15
  hostSupportsFeature,
16
+ isBusy,
14
17
  isRtcHello,
15
18
  organize,
16
19
  parseRtcMaxMessageSize,
17
- rtcSelectedPair
18
- } from "./chunk-xqycs43g.js";
20
+ rtcSelectedPair,
21
+ withEarlier
22
+ } from "./chunk-kmzzjv53.js";
19
23
  import {
20
24
  SECURE_PROLOGUE,
21
25
  SecureSession,
@@ -286,11 +290,12 @@ function webSocketTransport(url, token) {
286
290
  };
287
291
  }
288
292
 
289
- // packages/protocol/src/transcript.ts
290
- function isBusy(view) {
291
- const live = view.docs?.["pi.live"];
292
- return live?.run !== undefined;
293
+ // packages/protocol/src/terminals.ts
294
+ function isTerminalsHello(value) {
295
+ return value !== null && typeof value === "object" && typeof value.limit === "number";
293
296
  }
297
+ var terminalsStream = (id) => `terminals:${id}`;
298
+ var terminalStream = (id) => `terminal:${id}`;
294
299
 
295
300
  // packages/protocol/src/remote-durable.ts
296
301
  var DEFAULT_CANDIDATE_TIMEOUTS = {
@@ -389,6 +394,8 @@ class RemoteClient {
389
394
  #options;
390
395
  #listeners = new Set;
391
396
  #values = new Map;
397
+ #earlier = new Map;
398
+ #loadingEarlier = new Map;
392
399
  #wanted = new Set;
393
400
  #fresh = new Set;
394
401
  #docs = [];
@@ -458,6 +465,11 @@ class RemoteClient {
458
465
  #helloModels = [];
459
466
  #helloDefaults = {};
460
467
  #hostVersion;
468
+ #hostTerminals;
469
+ #terminalWatchers = new Map;
470
+ #terminalSinks = new Map;
471
+ #terminalPositions = new Map;
472
+ #terminalSizes = new Map;
461
473
  #attempt = 0;
462
474
  #rtcAttemptNonce = randomHex(8);
463
475
  #closed = false;
@@ -710,6 +722,7 @@ class RemoteClient {
710
722
  this.#helloModels = frame.models;
711
723
  this.#helloDefaults = frame.defaults ?? {};
712
724
  this.#hostVersion = frame.hostVersion;
725
+ this.#hostTerminals = isTerminalsHello(frame.terminals) ? frame.terminals : undefined;
713
726
  this.#fresh.clear();
714
727
  const covered = new Set(hostStreams(hostMinor));
715
728
  for (const name of HOST_STREAM_NAMES) {
@@ -727,6 +740,7 @@ class RemoteClient {
727
740
  if (this.#side !== undefined)
728
741
  for (const stream of this.#conversationStreams(this.#side))
729
742
  this.#wanted.add(stream);
743
+ this.#terminalsAvailability();
730
744
  for (const stream of this.#wanted)
731
745
  this.#subscribe(stream);
732
746
  if (frame.endpointSet !== undefined) {
@@ -815,6 +829,7 @@ class RemoteClient {
815
829
  this.#resolveReady({
816
830
  view: this.#viewSource(),
817
831
  controller: this.#controller(),
832
+ terminals: this.#terminals(),
818
833
  setConnectionMode: (mode) => this.setConnectionMode(mode),
819
834
  rtcDiagnostics: () => this.rtcDiagnostics(),
820
835
  close: () => this.#close()
@@ -828,6 +843,9 @@ class RemoteClient {
828
843
  await this.#recoverSubmissions();
829
844
  if (!this.#isActive(dial))
830
845
  return;
846
+ for (const [terminalId, size] of this.#terminalSizes) {
847
+ this.#callNow("resizeTerminal", { terminalId, ...size }).catch(() => {});
848
+ }
831
849
  this.#finishSyncGate();
832
850
  this.#afterCarrierReady(dial);
833
851
  }
@@ -1961,7 +1979,14 @@ class RemoteClient {
1961
1979
  case "snapshot":
1962
1980
  if (!this.#wanted.has(frame.stream))
1963
1981
  return;
1982
+ if (frame.stream.startsWith("terminals:")) {
1983
+ this.#values.set(frame.stream, frame.value);
1984
+ this.#notifyTerminals(frame.stream, { kind: "ready", list: frame.value });
1985
+ return;
1986
+ }
1964
1987
  this.#values.set(frame.stream, frame.value);
1988
+ this.#earlier.delete(frame.stream);
1989
+ this.#loadingEarlier.delete(frame.stream);
1965
1990
  this.#fresh.add(frame.stream);
1966
1991
  this.#streamVersions.set(frame.stream, (this.#streamVersions.get(frame.stream) ?? 0) + 1);
1967
1992
  this.#refresh();
@@ -1974,9 +1999,23 @@ class RemoteClient {
1974
1999
  this.#values.set(frame.stream, applyImmutable(this.#values.get(frame.stream), frame.ops));
1975
2000
  this.#refresh();
1976
2001
  return;
2002
+ case "output": {
2003
+ const sink = this.#terminalSinks.get(frame.stream);
2004
+ if (sink === undefined || !this.#wanted.has(frame.stream))
2005
+ return;
2006
+ this.#terminalPositions.set(frame.stream, frame.seq);
2007
+ sink.output(frame.data, frame.reset === true);
2008
+ return;
2009
+ }
1977
2010
  case "ended": {
2011
+ if (frame.stream.startsWith("terminals:") || frame.stream.startsWith("terminal:")) {
2012
+ this.#endTerminalStream(frame.stream, frame.reason);
2013
+ return;
2014
+ }
1978
2015
  this.#wanted.delete(frame.stream);
1979
2016
  this.#values.delete(frame.stream);
2017
+ this.#earlier.delete(frame.stream);
2018
+ this.#loadingEarlier.delete(frame.stream);
1980
2019
  this.#fresh.delete(frame.stream);
1981
2020
  const ended = new Error(`Stream ${frame.stream} ended: ${frame.reason}`);
1982
2021
  for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? []) {
@@ -2062,6 +2101,8 @@ class RemoteClient {
2062
2101
  continue;
2063
2102
  this.#wanted.delete(stream);
2064
2103
  this.#values.delete(stream);
2104
+ this.#earlier.delete(stream);
2105
+ this.#loadingEarlier.delete(stream);
2065
2106
  this.#fresh.delete(stream);
2066
2107
  for (const waiter of this.#snapshotWaiters.get(stream)?.splice(0) ?? [])
2067
2108
  waiter.resolve();
@@ -2070,7 +2111,162 @@ class RemoteClient {
2070
2111
  }
2071
2112
  #subscribe(stream) {
2072
2113
  const refs = stream.startsWith("conversation:") && hostSupportsFeature(this.#hostMinor, "imageRefs");
2073
- this.#send({ type: "subscribe", stream, ...refs ? { images: "ref" } : {} });
2114
+ const since = this.#terminalPositions.get(stream);
2115
+ const window = this.#window(stream);
2116
+ this.#send({
2117
+ type: "subscribe",
2118
+ stream,
2119
+ ...refs ? { images: "ref" } : {},
2120
+ ...since === undefined ? {} : { since },
2121
+ ...window === undefined ? {} : { window }
2122
+ });
2123
+ }
2124
+ #window(stream) {
2125
+ if (!stream.startsWith("conversation:") || !hostSupportsFeature(this.#hostMinor, "transcriptPaging"))
2126
+ return;
2127
+ const from = this.#shownView(stream)?.entries[0]?.id;
2128
+ return from === undefined ? { limit: TRANSCRIPT_PAGE } : { limit: TRANSCRIPT_PAGE, from };
2129
+ }
2130
+ #shownView(stream) {
2131
+ const view = this.#values.get(stream);
2132
+ return view === undefined ? undefined : withEarlier(view, this.#earlier.get(stream));
2133
+ }
2134
+ #loadEarlier(id) {
2135
+ const stream = conversationStream(id);
2136
+ const inFlight = this.#loadingEarlier.get(stream);
2137
+ if (inFlight !== undefined)
2138
+ return inFlight;
2139
+ const requested = this.#shownView(stream)?.before;
2140
+ if (requested === undefined)
2141
+ return Promise.resolve();
2142
+ const load = this.#call("earlierEntries", { conversationId: id, before: requested, limit: TRANSCRIPT_PAGE }).then((page) => {
2143
+ if (this.#loadingEarlier.get(stream) !== load)
2144
+ return;
2145
+ const shown = this.#shownView(stream);
2146
+ if (shown === undefined)
2147
+ return;
2148
+ const earlier = addEarlier(shown, this.#earlier.get(stream), requested, page);
2149
+ if (earlier === undefined)
2150
+ return;
2151
+ this.#earlier.set(stream, earlier);
2152
+ this.#refresh();
2153
+ }).finally(() => {
2154
+ if (this.#loadingEarlier.get(stream) === load)
2155
+ this.#loadingEarlier.delete(stream);
2156
+ });
2157
+ this.#loadingEarlier.set(stream, load);
2158
+ return load;
2159
+ }
2160
+ #terminalsUnavailable() {
2161
+ if (!hostSupportsFeature(this.#hostMinor, "terminals")) {
2162
+ return "The host is too old for terminals (needs protocol 5.27); run `pinomad upgrade` on the host";
2163
+ }
2164
+ if (this.#hostTerminals === undefined)
2165
+ return "The host couldn't load its terminal module; its log says which package is missing";
2166
+ return;
2167
+ }
2168
+ #terminalsAvailability() {
2169
+ const reason = this.#terminalsUnavailable();
2170
+ if (reason === undefined) {
2171
+ for (const stream of this.#terminalWatchers.keys())
2172
+ this.#wanted.add(stream);
2173
+ for (const stream of this.#terminalSinks.keys())
2174
+ this.#wanted.add(stream);
2175
+ return;
2176
+ }
2177
+ for (const stream of [...this.#terminalWatchers.keys()]) {
2178
+ this.#wanted.delete(stream);
2179
+ this.#values.delete(stream);
2180
+ this.#notifyTerminals(stream, { kind: "unavailable", reason });
2181
+ }
2182
+ for (const stream of [...this.#terminalSinks.keys()])
2183
+ this.#endTerminalStream(stream, reason);
2184
+ }
2185
+ #notifyTerminals(stream, state) {
2186
+ for (const listener of [...this.#terminalWatchers.get(stream) ?? []])
2187
+ listener(state);
2188
+ }
2189
+ #endTerminalStream(stream, reason) {
2190
+ this.#wanted.delete(stream);
2191
+ this.#values.delete(stream);
2192
+ if (stream.startsWith("terminals:")) {
2193
+ this.#notifyTerminals(stream, { kind: "unavailable", reason });
2194
+ return;
2195
+ }
2196
+ const sink = this.#terminalSinks.get(stream);
2197
+ this.#terminalSinks.delete(stream);
2198
+ this.#terminalPositions.delete(stream);
2199
+ this.#terminalSizes.delete(stream.slice("terminal:".length));
2200
+ sink?.ended(reason);
2201
+ }
2202
+ #terminalCall(method, args) {
2203
+ const reason = this.#terminalsUnavailable();
2204
+ return reason === undefined ? this.#call(method, args) : Promise.reject(new Error(reason));
2205
+ }
2206
+ #terminals() {
2207
+ return {
2208
+ watch: (conversationId, listener) => {
2209
+ const stream = terminalsStream(conversationId);
2210
+ let listeners = this.#terminalWatchers.get(stream);
2211
+ const first = listeners === undefined;
2212
+ if (listeners === undefined)
2213
+ this.#terminalWatchers.set(stream, listeners = new Set);
2214
+ listeners.add(listener);
2215
+ const reason = this.#terminalsUnavailable();
2216
+ const list = this.#values.get(stream);
2217
+ listener(reason !== undefined ? { kind: "unavailable", reason } : list !== undefined ? { kind: "ready", list } : { kind: "loading" });
2218
+ if (first && reason === undefined) {
2219
+ this.#wanted.add(stream);
2220
+ if (this.#helloReceived)
2221
+ this.#subscribe(stream);
2222
+ }
2223
+ return () => {
2224
+ if (!listeners.delete(listener) || listeners.size > 0)
2225
+ return;
2226
+ this.#terminalWatchers.delete(stream);
2227
+ if (!this.#wanted.delete(stream))
2228
+ return;
2229
+ this.#values.delete(stream);
2230
+ this.#send({ type: "unsubscribe", stream });
2231
+ };
2232
+ },
2233
+ attach: (terminalId, sink) => {
2234
+ const stream = terminalStream(terminalId);
2235
+ const reason = this.#terminalsUnavailable();
2236
+ if (reason !== undefined) {
2237
+ sink.ended(reason);
2238
+ return () => {};
2239
+ }
2240
+ if (this.#terminalSinks.has(stream) && this.#wanted.has(stream))
2241
+ this.#send({ type: "unsubscribe", stream });
2242
+ this.#terminalSinks.set(stream, sink);
2243
+ this.#terminalPositions.delete(stream);
2244
+ this.#wanted.add(stream);
2245
+ if (this.#helloReceived)
2246
+ this.#subscribe(stream);
2247
+ return () => {
2248
+ if (this.#terminalSinks.get(stream) !== sink)
2249
+ return;
2250
+ this.#terminalSinks.delete(stream);
2251
+ this.#terminalPositions.delete(stream);
2252
+ this.#terminalSizes.delete(terminalId);
2253
+ if (this.#wanted.delete(stream))
2254
+ this.#send({ type: "unsubscribe", stream });
2255
+ };
2256
+ },
2257
+ open: (conversationId, size) => this.#terminalCall("openTerminal", { conversationId, ...size }).then(({ terminalId }) => terminalId),
2258
+ input: (terminalId, data) => {
2259
+ this.#terminalCall("terminalInput", { terminalId, data }).catch(() => {});
2260
+ },
2261
+ resize: (terminalId, size) => {
2262
+ if (this.#terminalSinks.has(terminalStream(terminalId)))
2263
+ this.#terminalSizes.set(terminalId, size);
2264
+ this.#terminalCall("resizeTerminal", { terminalId, ...size }).catch(() => {});
2265
+ },
2266
+ rename: (terminalId, name) => this.#terminalCall("renameTerminal", { terminalId, name }).then(() => {}),
2267
+ restart: (terminalId, size) => this.#terminalCall("restartTerminal", { terminalId, ...this.#terminalSizes.get(terminalId) ?? size }).then(() => {}),
2268
+ close: (terminalId) => this.#terminalCall("closeTerminal", { terminalId }).then(() => {})
2269
+ };
2074
2270
  }
2075
2271
  #host(name) {
2076
2272
  return this.#values.get(name);
@@ -2172,22 +2368,33 @@ class RemoteClient {
2172
2368
  record.error = undefined;
2173
2369
  this.#clearSubmissionTimer(record);
2174
2370
  }
2175
- #entriesOf(id) {
2176
- if (id === undefined)
2177
- return [];
2178
- const view = this.#values.get(conversationStream(id));
2179
- return view?.entries ?? [];
2180
- }
2181
2371
  #entryPresent(record) {
2182
- if (record.entry === undefined)
2372
+ if (record.entry === undefined || record.conversationId === undefined)
2373
+ return false;
2374
+ const shown = this.#shownView(conversationStream(record.conversationId));
2375
+ if (shown === undefined)
2183
2376
  return false;
2184
- return this.#entriesOf(record.conversationId).some((entry) => String(entry.id) === String(record.entry));
2377
+ if (shown.before !== undefined && Number(record.entry) <= Number(shown.before))
2378
+ return true;
2379
+ return shown.entries.some((entry) => String(entry.id) === String(record.entry));
2380
+ }
2381
+ #inboxOf(id) {
2382
+ if (id === undefined)
2383
+ return;
2384
+ const view = this.#values.get(conversationStream(id));
2385
+ return view?.docs?.["pi.inbox"];
2185
2386
  }
2186
2387
  #inboxHas(record) {
2187
- if (record.conversationId === undefined || record.submissionId === undefined)
2388
+ if (record.submissionId === undefined)
2188
2389
  return false;
2189
- const inbox = this.#values.get(docStream("pi.inbox", record.conversationId));
2190
- return (inbox?.items ?? []).some((item) => String(item.id) === String(record.submissionId));
2390
+ const items = this.#inboxOf(record.conversationId)?.items ?? [];
2391
+ return items.some((item) => String(item.id) === String(record.submissionId));
2392
+ }
2393
+ #markChecked(record) {
2394
+ if (record.conversationId === undefined)
2395
+ return;
2396
+ record.checkedVersion = this.#streamVersions.get(conversationStream(record.conversationId)) ?? 0;
2397
+ record.checkedInbox = this.#inboxOf(record.conversationId);
2191
2398
  }
2192
2399
  #applySubmissionRecord(record, found) {
2193
2400
  record.admitted = true;
@@ -2218,11 +2425,10 @@ class RemoteClient {
2218
2425
  this.#settleSubmission(record);
2219
2426
  continue;
2220
2427
  }
2221
- if (record.conversationId !== undefined && record.submissionId !== undefined && record.admitted && (record.entry !== undefined || !this.#inboxHas(record))) {
2428
+ if (record.conversationId !== undefined && !record.confirming && record.admitted && (record.entry !== undefined || !this.#inboxHas(record))) {
2222
2429
  const stream = conversationStream(record.conversationId);
2223
- const version = (this.#streamVersions.get(stream) ?? 0) + (this.#streamVersions.get(docStream("pi.inbox", record.conversationId)) ?? 0);
2224
- if (this.#fresh.has(stream) && record.checkedVersion !== version) {
2225
- record.checkedVersion = version;
2430
+ if (this.#fresh.has(stream) && (record.checkedVersion !== (this.#streamVersions.get(stream) ?? 0) || record.checkedInbox !== this.#inboxOf(record.conversationId))) {
2431
+ this.#markChecked(record);
2226
2432
  this.#confirmSubmission(record);
2227
2433
  }
2228
2434
  }
@@ -2256,6 +2462,8 @@ class RemoteClient {
2256
2462
  } catch {} finally {
2257
2463
  record.confirming = false;
2258
2464
  }
2465
+ if (!this.#closed && !this.#submissionSettled(record))
2466
+ this.#reconcileSubmissions();
2259
2467
  }
2260
2468
  #submissionArgs(record) {
2261
2469
  const conversationId = record.conversationId;
@@ -2299,11 +2507,11 @@ class RemoteClient {
2299
2507
  record.phase = "inFlight";
2300
2508
  if (this.#entryPresent(record))
2301
2509
  this.#settleSubmission(record);
2510
+ } else if (this.#inboxHas(record)) {
2511
+ this.#settleSubmission(record);
2302
2512
  } else {
2303
2513
  record.phase = "inFlight";
2304
- if (record.conversationId !== undefined) {
2305
- record.checkedVersion = (this.#streamVersions.get(conversationStream(record.conversationId)) ?? 0) + (this.#streamVersions.get(docStream("pi.inbox", record.conversationId)) ?? 0);
2306
- }
2514
+ this.#markChecked(record);
2307
2515
  this.#confirmSubmission(record);
2308
2516
  }
2309
2517
  this.#touchSubmissions();
@@ -2549,7 +2757,7 @@ class RemoteClient {
2549
2757
  this.#reconcileSubmissions();
2550
2758
  const current = this.#current;
2551
2759
  const side = this.#side;
2552
- const conversation = current === undefined ? undefined : this.#values.get(conversationStream(current));
2760
+ const conversation = current === undefined ? undefined : this.#shownView(conversationStream(current));
2553
2761
  const summaries = this.#host("conversations") ?? [];
2554
2762
  const index = this.#host("index") ?? { projects: [], conversations: [] };
2555
2763
  const devices = this.#host("devices")?.devices ?? [];
@@ -2565,7 +2773,7 @@ class RemoteClient {
2565
2773
  submissions: this.#activeSubmissions(),
2566
2774
  side: side === undefined ? undefined : {
2567
2775
  id: side,
2568
- conversation: this.#values.get(conversationStream(side)),
2776
+ conversation: this.#shownView(conversationStream(side)),
2569
2777
  docs: this.#docsOf(side)
2570
2778
  },
2571
2779
  devices,
@@ -2797,6 +3005,17 @@ class RemoteClient {
2797
3005
  });
2798
3006
  return outcome;
2799
3007
  }),
3008
+ reorderQueued: (submissionId, before, target) => this.#request(async () => {
3009
+ if (!hostSupportsCall(this.#hostMinor, "reorderQueued")) {
3010
+ throw new Error("The host is too old for this (needs protocol 5.29); run `pinomad upgrade` on the host");
3011
+ }
3012
+ const { outcome } = await this.#call("reorderQueued", {
3013
+ conversationId: target ?? conversationId(),
3014
+ submissionId,
3015
+ ...before === undefined ? {} : { before }
3016
+ });
3017
+ return outcome;
3018
+ }),
2800
3019
  setThinkingLevel: (level) => this.#command(() => this.#call("setThinkingLevel", { conversationId: conversationId(), level })),
2801
3020
  setModel: (model) => this.#command(() => this.#call("setModel", { conversationId: conversationId(), model })),
2802
3021
  setAgent: (agent) => this.#command(() => {
@@ -2865,6 +3084,7 @@ class RemoteClient {
2865
3084
  }
2866
3085
  return this.#call("listSkills", { scope });
2867
3086
  },
3087
+ loadEarlier: (id) => this.#loadEarlier(id),
2868
3088
  listCommands: (id) => {
2869
3089
  if (!hostSupportsCall(this.#hostMinor, "listCommands")) {
2870
3090
  return Promise.reject(new Error("The host is too old for this (needs protocol 5.17); run `pinomad upgrade` on the host"));