@volter/supercode-terminal 0.2.59 → 0.2.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -157,6 +157,8 @@ export class TerminalWebSocketBridge {
157
157
 
158
158
  export class TmuxTerminalHost {
159
159
  #allowUnownedControl;
160
+ #generation = 0;
161
+ #listing = null;
160
162
  #attachments = new Map();
161
163
  #attachSocket;
162
164
  #catalog = new Map();
@@ -186,7 +188,19 @@ export class TmuxTerminalHost {
186
188
  this.#ttlMs = positiveInteger(options.attachmentTtlMs ?? DEFAULT_ATTACHMENT_TTL_MS, 'attachmentTtlMs');
187
189
  }
188
190
 
189
- async listSessions() {
191
+ /**
192
+ * The panes, as tmux lists them. Callers asking while a listing is running share it (one listing is five tmux
193
+ * processes, and a daemon's doors ask together); a change made through this host since starts a fresh one.
194
+ */
195
+ listSessions() {
196
+ if (this.#listing && this.#listing.generation === this.#generation) return this.#listing.promise;
197
+ const listing = { generation: this.#generation, promise: null };
198
+ listing.promise = this.#listSessions().finally(() => { if (this.#listing === listing) this.#listing = null; });
199
+ this.#listing = listing;
200
+ return listing.promise;
201
+ }
202
+
203
+ async #listSessions() {
190
204
  this.#pruneAttachments();
191
205
  const names = this.#sessionNames();
192
206
  const liveNames = new Set(names);
@@ -215,6 +229,8 @@ export class TmuxTerminalHost {
215
229
  label: name,
216
230
  owned: ownedNames.has(name),
217
231
  size: size ? Object.freeze({ columns: size.width, rows: size.height }) : null,
232
+ // When the pane last printed, as tmux keeps it (milliseconds): idle time without a screen capture.
233
+ lastOutputAt: size?.activity ? size.activity * 1000 : null,
218
234
  });
219
235
  });
220
236
  }
@@ -246,6 +262,8 @@ export class TmuxTerminalHost {
246
262
  settings: TERMINAL_LIKE_SESSION,
247
263
  socket: this.#socket,
248
264
  });
265
+ // Raised once tmux has the session: a listing started before then cannot be the one that answers below.
266
+ this.#generation += 1;
249
267
  const id = this.#catalogId(name);
250
268
  const created = (await this.listSessions()).find((session) => session.id === id);
251
269
  if (!created) {
@@ -285,6 +303,7 @@ export class TmuxTerminalHost {
285
303
  throw new TerminalHostError('bind_not_allowed', 'This host does not own that tmux session.');
286
304
  }
287
305
  await this.#tmux.setSessionOptionAsync(entry.name, this.#contextOption, contextKey, this.#socket);
306
+ this.#generation += 1;
288
307
  const bound = (await this.listSessions()).find((session) => session.id === sessionId);
289
308
  if (!bound) {
290
309
  throw new TerminalHostError('session_not_found', 'The tmux session exited before it could be bound.');
@@ -372,6 +391,7 @@ export class TmuxTerminalHost {
372
391
  throw new TerminalHostError('close_not_allowed', 'This host does not own that tmux session.');
373
392
  }
374
393
  this.#tmux.killSessionProcessTree({ session: entry.name, socket: this.#socket });
394
+ this.#generation += 1;
375
395
  this.#catalog.delete(sessionId);
376
396
  this.#idByName.delete(entry.name);
377
397
  for (const [id, grant] of this.#attachments) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.59",
3
+ "version": "0.2.60",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {
@@ -8,6 +8,8 @@ export type TmuxPane = {
8
8
  export type TmuxWindowSize = {
9
9
  width: number;
10
10
  height: number;
11
+ /** When the window last had output, in Unix seconds (tmux's `window_activity`); absent when tmux did not say. */
12
+ activity?: number;
11
13
  };
12
14
  export type TmuxCommand = {
13
15
  arguments?: string[];
package/substrate/tmux.js CHANGED
@@ -670,7 +670,7 @@ export async function listWindowSizesAsync(socket) {
670
670
  "list-windows",
671
671
  "-a",
672
672
  "-F",
673
- `#{session_name}${separator}#{window_index}${separator}#{window_width}${separator}#{window_height}`
673
+ `#{session_name}${separator}#{window_index}${separator}#{window_width}${separator}#{window_height}${separator}#{window_activity}`
674
674
  ]))).trim();
675
675
  }
676
676
  catch (error) {
@@ -692,7 +692,7 @@ export async function listWindowSizesAsync(socket) {
692
692
  return sizes;
693
693
  }
694
694
  for (const line of output.split("\n")) {
695
- const [session, windowIndex, width, height] = line.split(separator);
695
+ const [session, windowIndex, width, height, activity] = line.split(separator);
696
696
  // Match getWindowSize, which reads window 0; ignore extra windows of a session.
697
697
  if (!session || windowIndex !== "0") {
698
698
  continue;
@@ -700,7 +700,8 @@ export async function listWindowSizesAsync(socket) {
700
700
  const parsedWidth = Number(width);
701
701
  const parsedHeight = Number(height);
702
702
  if (Number.isInteger(parsedWidth) && Number.isInteger(parsedHeight) && parsedWidth >= 1 && parsedHeight >= 1) {
703
- sizes.set(session, { height: parsedHeight, width: parsedWidth });
703
+ const parsedActivity = Number(activity);
704
+ sizes.set(session, { height: parsedHeight, width: parsedWidth, ...(Number.isInteger(parsedActivity) && parsedActivity > 0 ? { activity: parsedActivity } : {}) });
704
705
  }
705
706
  }
706
707
  return sizes;