@volter/supercode-terminal 0.2.69 → 0.2.71

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/README.md CHANGED
@@ -42,6 +42,16 @@ the fleet tool package dependency. Advanced hosts and legacy adapters may import
42
42
  subpaths are exported only for compatibility. Prefer the controller/client APIs
43
43
  above this layer for new applications.
44
44
 
45
+ `TmuxTerminalHost`, `PtyTerminalHost` and `PaneServerHost` expose
46
+ `hasLiveContext(contextKey)` for decisions that require a complete presence
47
+ reading. Tmux reads all session IDs and context values in one command, without
48
+ the ordinary option reader's best-effort fallback. Failed, incomplete or
49
+ malformed identity reads reject; only an explicit no-server answer establishes
50
+ emptiness after a failed command. Other connection errors remain unknown. The
51
+ PTY host reads its own live catalog; an older pane server refuses this operation.
52
+ This is a host observation, not a physical window/tab reading or an existence
53
+ lock against a subsequent launch.
54
+
45
55
  The substrate was moved from a retired package without behavioral changes.
46
56
  Its existing home paths, environment variables, and tmux ownership markers stay
47
57
  compatible. Those files retain Apache-2.0 licensing; see `NOTICE-SUBSTRATE` and
package/index.d.ts CHANGED
@@ -49,6 +49,8 @@ export type TmuxTerminalHostOptions = {
49
49
  export class TmuxTerminalHost {
50
50
  constructor(options?: TmuxTerminalHostOptions);
51
51
  listSessions(): Promise<TerminalSession[]>;
52
+ /** Complete context observation; failures reject rather than report absence. */
53
+ hasLiveContext(contextKey: string): Promise<boolean>;
52
54
  createSession(input: {
53
55
  command: TmuxCommand;
54
56
  contextKey?: string;
@@ -87,6 +89,8 @@ export class TmuxTerminalHost {
87
89
  export class PaneServerHost {
88
90
  constructor(options: { pipe: string; serverScript?: string });
89
91
  listSessions(): Promise<TerminalSession[]>;
92
+ /** Requires a compatible pane server; an unsupported operation rejects. */
93
+ hasLiveContext(contextKey: string): Promise<boolean>;
90
94
  createSession(input: { command: TmuxCommand; contextKey?: string; cwd?: string; env?: Record<string, string>; name?: string }): Promise<TerminalSession>;
91
95
  capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): Promise<string>;
92
96
  sendInput(sessionId: string, data: string, options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' | 'none'; inputOwner?: string }): Promise<TerminalInputDelivery>;
@@ -112,6 +116,8 @@ export class PaneServerHost {
112
116
  export class PtyTerminalHost {
113
117
  constructor(options?: { attachmentTtlMs?: number });
114
118
  listSessions(): Promise<TerminalSession[]>;
119
+ /** Live context membership in this host's own pane catalog. */
120
+ hasLiveContext(contextKey: string): boolean;
115
121
  createSession(input: {
116
122
  command: TmuxCommand;
117
123
  contextKey?: string;
package/index.mjs CHANGED
@@ -215,6 +215,12 @@ export class TmuxTerminalHost {
215
215
  return listing.promise;
216
216
  }
217
217
 
218
+ /** Complete context observation for callers deciding whether a pane has ended. */
219
+ hasLiveContext(contextKey) {
220
+ assertContextKey(contextKey);
221
+ return this.#tmux.hasSessionContextAsync(this.#contextOption, contextKey, this.#socket);
222
+ }
223
+
218
224
  async #listSessions() {
219
225
  this.#pruneAttachments();
220
226
  const names = this.#sessionNames();
@@ -637,6 +643,11 @@ export class PtyTerminalHost {
637
643
  return [...this.#sessions.values()].map((entry) => this.#describe(entry, table));
638
644
  }
639
645
 
646
+ hasLiveContext(contextKey) {
647
+ assertContextKey(contextKey);
648
+ return [...this.#sessions.values()].some((entry) => entry.contextKey === contextKey && entry.exit === null);
649
+ }
650
+
640
651
  async createSession(input) {
641
652
  const name = input.name ?? `supercode-${this.#randomToken().slice(0, 16)}`;
642
653
  if (!SESSION_NAME.test(name)) {
@@ -1105,6 +1116,7 @@ export class PaneServerHost {
1105
1116
  }
1106
1117
 
1107
1118
  listSessions() { return this.#request('listSessions', []); }
1119
+ hasLiveContext(contextKey) { assertContextKey(contextKey); return this.#request('hasLiveContext', [contextKey]); }
1108
1120
  createSession(input) { return this.#request('createSession', [input]); }
1109
1121
  capture(sessionId, options = {}) { return this.#request('capture', [sessionId, options]); }
1110
1122
  sendInput(sessionId, data, options = {}) { return this.#request('sendInput', [sessionId, data, options]); }
@@ -1908,7 +1920,11 @@ export function composerText(kind, screen) {
1908
1920
  // the live bottom (model/cwd and shortcuts can occupy separate rows).
1909
1921
  if (kind === 'codex' && (plain.slice(end + 1).some((line) => line.trim() && !footer.test(line)) ||
1910
1922
  /\bmodel:\s+loading\b/iu.test(plain.findLast((line) => /\bmodel:/iu.test(line)) ?? ''))) return null;
1911
- if (kind === 'claude-code' && (row === 0 || !rule.test(plain[row - 1]))) return null;
1923
+ if (kind === 'claude-code' && (row === 0 || !rule.test(plain[row - 1]) ||
1924
+ // Prompt-history search keeps the editor's caret and enclosing rules,
1925
+ // but its input is a search query. Its live footer can wrap on a narrow
1926
+ // pane; inspect only rows below this editor, never transcript text.
1927
+ /(?:^|\n)\s*search\s+prompts\s*:/iu.test(plain.slice(end + 1).join('\n')))) return null;
1912
1928
  const first = plain[row].match(caret)[0].length;
1913
1929
  const value = lines.slice(row, end).map((cells, index) => cells
1914
1930
  .slice(index === 0 || /^\s*$/u.test(plain[row + index].slice(0, first)) ? first : 0).filter((cell) => !cell.placeholder)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.69",
3
+ "version": "0.2.71",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {
@@ -32,7 +32,7 @@ function leave() {
32
32
  setTimeout(() => { server.close(); for (const connection of clients) connection.end(); setTimeout(done, 500); }, 3_000).unref?.();
33
33
  }
34
34
 
35
- const OPS = new Set(['listSessions', 'createSession', 'capture', 'sendInput', 'acquireInput', 'ownsInput', 'releaseInput', 'closeSession', 'renameWindow', 'issueAttachment', 'bindContext']);
35
+ const OPS = new Set(['listSessions', 'hasLiveContext', 'createSession', 'capture', 'sendInput', 'acquireInput', 'ownsInput', 'releaseInput', 'closeSession', 'renameWindow', 'issueAttachment', 'bindContext']);
36
36
 
37
37
  function scheduleIdleExit() {
38
38
  clearTimeout(idleTimer);
@@ -34,6 +34,8 @@ export declare function createSession({ command, cwd, env, name, owner, ownerOpt
34
34
  name: string;
35
35
  panes: number;
36
36
  }>;
37
+ /** A complete context reading for absence decisions; ordinary option listings are best-effort. */
38
+ export declare function hasSessionContextAsync(option: string, contextKey: string, socket?: string): Promise<boolean>;
37
39
  /** Read one namespaced user option for every live session in one tmux call. */
38
40
  export declare function sessionOptionValuesAsync(option: string, socket?: string): Promise<Map<string, string>>;
39
41
  /** Set one of tmux's own session options on one session. */
package/substrate/tmux.js CHANGED
@@ -151,6 +151,39 @@ function assertUserOption(option) {
151
151
  // value (opaque, colons allowed) takes the whole remainder. Same rule as
152
152
  // `listPanesFormat`: printable separator, free-text field last.
153
153
  const sessionValueSeparator = ":";
154
+ /** A complete context reading for absence decisions; ordinary option listings are best-effort. */
155
+ export async function hasSessionContextAsync(option, contextKey, socket) {
156
+ assertTmux();
157
+ assertUserOption(option);
158
+ let output;
159
+ try {
160
+ output = await runAsync("tmux", tmuxArgs(socket, ["list-sessions", "-F", `#{session_id}${sessionValueSeparator}#{${option}}`]), { timeoutMs: 5_000 });
161
+ }
162
+ catch (error) {
163
+ // Only tmux's explicit no-server answer establishes emptiness. Connection,
164
+ // permission, missing-command and timeout errors remain unread observations.
165
+ if (error instanceof Error && /\nno server running on [^\r\n]+$/.test(error.message))
166
+ return false;
167
+ throw error;
168
+ }
169
+ // Successful list-sessions has at least one complete row. Include unbound
170
+ // sessions, reject malformed/duplicate identities, and inspect every row even
171
+ // after a match so partial identity output cannot establish absence.
172
+ if (!output.endsWith("\n"))
173
+ throw new Error("tmux context reading is incomplete");
174
+ const ids = new Set();
175
+ let present = false;
176
+ for (const line of output.slice(0, -1).split("\n")) {
177
+ const separator = line.indexOf(sessionValueSeparator);
178
+ const id = line.slice(0, separator);
179
+ if (separator < 1 || !/^\$\d+$/.test(id) || ids.has(id))
180
+ throw new Error("tmux context reading has an invalid session identity");
181
+ ids.add(id);
182
+ if (line.slice(separator + 1) === contextKey)
183
+ present = true;
184
+ }
185
+ return present;
186
+ }
154
187
  /** Read one namespaced user option for every live session in one tmux call. */
155
188
  export async function sessionOptionValuesAsync(option, socket) {
156
189
  assertTmux();