@volter/supercode-terminal 0.2.70 → 0.2.72
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/CAPABILITIES.md +1 -1
- package/README.md +17 -0
- package/index.d.ts +8 -1
- package/index.mjs +42 -6
- package/package.json +1 -1
- package/substrate/pane-server.mjs +1 -1
- package/substrate/tmux.d.ts +2 -0
- package/substrate/tmux.js +33 -0
package/CAPABILITIES.md
CHANGED
|
@@ -23,7 +23,7 @@ means it must not be presented as supported.
|
|
|
23
23
|
| Scrollback, selection and copy | UI/xterm | verified | Bounded scrollback and native xterm selection. |
|
|
24
24
|
| Find in one terminal, next/previous match | UI | verified | Search covers the active xterm buffer. Cross-tab search is missing. |
|
|
25
25
|
| URL detection | UI | partial | The stable xterm web-link provider is active; host policy and end-to-end link opening still need a dedicated proof. |
|
|
26
|
-
| Exclusive input ownership | tmux/host | partial | `acquireInput`, `ownsInput` and `releaseInput` serialize a paste with other input and remain with the pane across a client restart. [Recorded tmux proof](../../docs/evidence/claude-input-2026-10-07.md#recorded-review-fixes) rejects a replacement while the badge is unrecorded, then confirms one native message after daemon restart. Older pane servers refuse this door; Windows ownership restart
|
|
26
|
+
| Exclusive input ownership | tmux/host | partial | `acquireInput`, `ownsInput` and `releaseInput` serialize a paste with other input and remain with the pane across a client restart. [Recorded tmux proof](../../docs/evidence/claude-input-2026-10-07.md#recorded-review-fixes) rejects a replacement while the badge is unrecorded, then confirms one native message after daemon restart. `PaneServerHost.createSession` can require this ABI before creating a pane; [manual old/current server admission](../../docs/evidence/launch-input-ownership-admission-2026-10-09.md) is recorded on macOS. Older pane servers refuse this door; Windows ownership restart and admission remain unmeasured. |
|
|
27
27
|
| Paste and bracketed paste | UI | verified | Multiline or large pastes require confirmation; history is memory-only. Advanced transforms and slow paste are missing. |
|
|
28
28
|
| Save terminal contents | UI | partial | The normal buffer is projected to a bounded plain-text download; the browser download boundary and continuous logging still need dedicated proof. |
|
|
29
29
|
| Bell and background activity | UI | verified | A controlled tmux shell proved active visual bell plus inactive activity/bell tab state. System notifications are host policy. |
|
package/README.md
CHANGED
|
@@ -42,6 +42,23 @@ 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
|
+
|
|
55
|
+
`PaneServerHost.createSession({ ..., requireInputOwnership: true })` requires
|
|
56
|
+
the connected server's ownership operation before creating a pane. An older
|
|
57
|
+
server rejects with `input_ownership_unsupported` and starts no process; the
|
|
58
|
+
same connection handles admission and creation. Omitting the option preserves
|
|
59
|
+
ordinary creation. [Manual old/current server SDK readings](../../docs/evidence/launch-input-ownership-admission-2026-10-09.md)
|
|
60
|
+
do not qualify Windows or native harness first-message delivery.
|
|
61
|
+
|
|
45
62
|
The substrate was moved from a retired package without behavioral changes.
|
|
46
63
|
Its existing home paths, environment variables, and tmux ownership markers stay
|
|
47
64
|
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,7 +89,10 @@ export class TmuxTerminalHost {
|
|
|
87
89
|
export class PaneServerHost {
|
|
88
90
|
constructor(options: { pipe: string; serverScript?: string });
|
|
89
91
|
listSessions(): Promise<TerminalSession[]>;
|
|
90
|
-
|
|
92
|
+
/** Requires a compatible pane server; an unsupported operation rejects. */
|
|
93
|
+
hasLiveContext(contextKey: string): Promise<boolean>;
|
|
94
|
+
/** Require the running pane server's ownership door before creating a pane. */
|
|
95
|
+
createSession(input: { command: TmuxCommand; contextKey?: string; cwd?: string; env?: Record<string, string>; name?: string; requireInputOwnership?: boolean }): Promise<TerminalSession>;
|
|
91
96
|
capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): Promise<string>;
|
|
92
97
|
sendInput(sessionId: string, data: string, options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' | 'none'; inputOwner?: string }): Promise<TerminalInputDelivery>;
|
|
93
98
|
/** Exclusive input ownership, retained by the pane host across client restart. */
|
|
@@ -112,6 +117,8 @@ export class PaneServerHost {
|
|
|
112
117
|
export class PtyTerminalHost {
|
|
113
118
|
constructor(options?: { attachmentTtlMs?: number });
|
|
114
119
|
listSessions(): Promise<TerminalSession[]>;
|
|
120
|
+
/** Live context membership in this host's own pane catalog. */
|
|
121
|
+
hasLiveContext(contextKey: string): boolean;
|
|
115
122
|
createSession(input: {
|
|
116
123
|
command: TmuxCommand;
|
|
117
124
|
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,7 +1116,8 @@ export class PaneServerHost {
|
|
|
1105
1116
|
}
|
|
1106
1117
|
|
|
1107
1118
|
listSessions() { return this.#request('listSessions', []); }
|
|
1108
|
-
|
|
1119
|
+
hasLiveContext(contextKey) { assertContextKey(contextKey); return this.#request('hasLiveContext', [contextKey]); }
|
|
1120
|
+
createSession(input) { return this.#request('createSession', [input], { requireInputOwnership: input.requireInputOwnership === true }); }
|
|
1109
1121
|
capture(sessionId, options = {}) { return this.#request('capture', [sessionId, options]); }
|
|
1110
1122
|
sendInput(sessionId, data, options = {}) { return this.#request('sendInput', [sessionId, data, options]); }
|
|
1111
1123
|
acquireInput(sessionId, owner) { return this.#request('acquireInput', [sessionId, owner]); }
|
|
@@ -1163,12 +1175,27 @@ export class PaneServerHost {
|
|
|
1163
1175
|
// A server that is leaving refuses a request unrun (`pane_server_restarting`): it is asked again of the next one.
|
|
1164
1176
|
for (let attempt = 0; ; attempt += 1) {
|
|
1165
1177
|
const connection = await this.#connect();
|
|
1166
|
-
const id = this.#next++;
|
|
1167
1178
|
try {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1179
|
+
if (op === 'createSession' && extra.requireInputOwnership) {
|
|
1180
|
+
// The installed client can be newer than the server that still holds
|
|
1181
|
+
// this machine's panes. Check its actual ownership door before any
|
|
1182
|
+
// process starts, on the same connection that will create the pane.
|
|
1183
|
+
// null cannot identify a PtyTerminalHost session: session_not_found
|
|
1184
|
+
// means the vendor's ownership operation ran without touching a pane.
|
|
1185
|
+
try {
|
|
1186
|
+
await this.#call(connection, 'ownsInput', [null, 'capability_check']);
|
|
1187
|
+
throw new TerminalHostError('input_ownership_unconfirmed', 'The pane server did not confirm its input ownership door; no pane was created.');
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (error?.code === 'unsupported') {
|
|
1190
|
+
throw new TerminalHostError('input_ownership_unsupported', 'The running pane server does not support input ownership required for this first message; no pane was created. Update the pane server through its owner before retrying.');
|
|
1191
|
+
}
|
|
1192
|
+
if (error?.code !== 'session_not_found') throw error;
|
|
1193
|
+
}
|
|
1194
|
+
if (connection.destroyed || this.#connection !== connection) {
|
|
1195
|
+
throw new TerminalHostError('pane_server_unreachable', 'The pane server connection changed during input ownership admission; no pane was created.');
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return await this.#call(connection, op, args, { ...extra, requireInputOwnership: undefined });
|
|
1172
1199
|
} catch (error) {
|
|
1173
1200
|
if (error?.code !== 'pane_server_restarting' || attempt >= 20) throw error;
|
|
1174
1201
|
await sleep(250);
|
|
@@ -1176,6 +1203,15 @@ export class PaneServerHost {
|
|
|
1176
1203
|
}
|
|
1177
1204
|
}
|
|
1178
1205
|
|
|
1206
|
+
#call(connection, op, args, extra = {}) {
|
|
1207
|
+
const id = this.#next++;
|
|
1208
|
+
return new Promise((resolve, reject) => {
|
|
1209
|
+
this.#pending.set(id, { resolve, reject, connection });
|
|
1210
|
+
try { connection.write(`${JSON.stringify({ id, op, args, ...extra })}\n`); }
|
|
1211
|
+
catch (error) { this.#pending.delete(id); reject(error); }
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1179
1215
|
#connect() {
|
|
1180
1216
|
if (this.#connection && !this.#connection.destroyed) return Promise.resolve(this.#connection);
|
|
1181
1217
|
return (this.#connecting ??= this.#open().finally(() => { this.#connecting = null; }));
|
package/package.json
CHANGED
|
@@ -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);
|
package/substrate/tmux.d.ts
CHANGED
|
@@ -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();
|