tmux-ide 2.9.3 → 2.9.5
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/LICENSE +1 -1
- package/README.md +120 -139
- package/bin/cli.js +4847 -3585
- package/package.json +16 -9
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
- package/skill/SKILL.md +30 -30
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code agent teams — opt-out enablement in the user's Claude settings.
|
|
3
|
+
*
|
|
4
|
+
* Agent teams let one Claude Code session spawn teammates; with
|
|
5
|
+
* `teammateMode: "auto"` Claude puts them in split panes when it runs inside
|
|
6
|
+
* tmux (in-process elsewhere), which is exactly what tmux-ide shows and
|
|
7
|
+
* routes. The installer enables this by default and `tmux-ide integration
|
|
8
|
+
* agent-teams enable|disable|status` manages it explicitly.
|
|
9
|
+
*
|
|
10
|
+
* Rules (pure, in {@link mergeAgentTeams}):
|
|
11
|
+
* - `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set to "1" only when absent.
|
|
12
|
+
* An explicit off value ("0", "false", false, 0…) is the user's opt-out: the
|
|
13
|
+
* installer never overrides it and touches nothing else. Only an explicit
|
|
14
|
+
* `tmux-ide integration agent-teams enable` (overrideOptOut) turns it back on.
|
|
15
|
+
* - `teammateMode: "auto"` is set only when `teammateMode` is absent.
|
|
16
|
+
* - Every other key is preserved. Nothing changed → nothing written.
|
|
17
|
+
*
|
|
18
|
+
* Writes (io) reuse the hooks integration's settings path resolution, including
|
|
19
|
+
* the `TMUX_IDE_CLAUDE_SETTINGS` override: one-time timestamped backup before
|
|
20
|
+
* the first modification, then an atomic temp-file + rename. Invalid JSON is
|
|
21
|
+
* reported and never rewritten.
|
|
22
|
+
*/
|
|
23
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, copyFileSync, unlinkSync, writeFileSync, accessSync, constants, } from "node:fs";
|
|
24
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
25
|
+
import { resolveRuntimeNamespace, runtimeOwnedPath } from "../../lib/runtime-namespace.js";
|
|
26
|
+
export const AGENT_TEAMS_ENV = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS";
|
|
27
|
+
export const DEFAULT_TEAMMATE_MODE = "auto";
|
|
28
|
+
const BACKUP_INFIX = ".tmux-ide-backup-";
|
|
29
|
+
/** PURE — an explicit off value is the user's opt-out. */
|
|
30
|
+
export function isOptOut(value) {
|
|
31
|
+
if (value === false || value === 0)
|
|
32
|
+
return true;
|
|
33
|
+
return typeof value === "string" && /^\s*(0|false|off|no)\s*$/iu.test(value);
|
|
34
|
+
}
|
|
35
|
+
function envOf(settings) {
|
|
36
|
+
const env = settings.env;
|
|
37
|
+
if (env === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
40
|
+
throw new Error("settings env must be an object");
|
|
41
|
+
return env;
|
|
42
|
+
}
|
|
43
|
+
/** PURE — how the settings currently stand for agent teams. */
|
|
44
|
+
export function agentTeamsSetting(settings) {
|
|
45
|
+
const value = envOf(settings)?.[AGENT_TEAMS_ENV];
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return "not-configured";
|
|
48
|
+
return isOptOut(value) ? "disabled-by-user" : "enabled";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* PURE — enable agent teams without overriding anything the user chose, unless
|
|
52
|
+
* `overrideOptOut` (the user explicitly asked to enable) replaces their "0".
|
|
53
|
+
*/
|
|
54
|
+
export function mergeAgentTeams(settings, { overrideOptOut = false } = {}) {
|
|
55
|
+
const setting = agentTeamsSetting(settings);
|
|
56
|
+
if (setting === "disabled-by-user" && !overrideOptOut)
|
|
57
|
+
return { settings, changes: [], setting };
|
|
58
|
+
const changes = [];
|
|
59
|
+
let next = settings;
|
|
60
|
+
if (setting !== "enabled") {
|
|
61
|
+
next = { ...next, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "1" } };
|
|
62
|
+
changes.push(`env.${AGENT_TEAMS_ENV}=1`);
|
|
63
|
+
}
|
|
64
|
+
if (!("teammateMode" in settings)) {
|
|
65
|
+
next = { ...next, teammateMode: DEFAULT_TEAMMATE_MODE };
|
|
66
|
+
changes.push(`teammateMode=${DEFAULT_TEAMMATE_MODE}`);
|
|
67
|
+
}
|
|
68
|
+
return { settings: next, changes, setting: "enabled" };
|
|
69
|
+
}
|
|
70
|
+
/** PURE — record an explicit opt-out ("0"), which later enables respect. */
|
|
71
|
+
export function disableAgentTeams(settings) {
|
|
72
|
+
if (agentTeamsSetting(settings) === "disabled-by-user")
|
|
73
|
+
return { settings, changes: [], setting: "disabled-by-user" };
|
|
74
|
+
return {
|
|
75
|
+
settings: { ...settings, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "0" } },
|
|
76
|
+
changes: [`env.${AGENT_TEAMS_ENV}=0`],
|
|
77
|
+
setting: "disabled-by-user",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function agentTeamsPaths() {
|
|
81
|
+
const namespace = resolveRuntimeNamespace();
|
|
82
|
+
return { settingsPath: namespace.claudeSettingsPath, claudeDir: namespace.claudeDir };
|
|
83
|
+
}
|
|
84
|
+
/** `claude` on PATH, without spawning anything. */
|
|
85
|
+
export function claudeOnPath(path = process.env.PATH ?? "") {
|
|
86
|
+
return path
|
|
87
|
+
.split(delimiter)
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.some((dir) => {
|
|
90
|
+
try {
|
|
91
|
+
accessSync(join(dir, "claude"), constants.X_OK);
|
|
92
|
+
return statSync(join(dir, "claude")).isFile();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export class InvalidClaudeSettingsError extends Error {
|
|
100
|
+
}
|
|
101
|
+
function readSettings(path) {
|
|
102
|
+
if (!existsSync(path))
|
|
103
|
+
return { settings: {}, exists: false };
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
106
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
107
|
+
throw new Error("not an object");
|
|
108
|
+
envOf(parsed);
|
|
109
|
+
return { settings: parsed, exists: true };
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new InvalidClaudeSettingsError(`${path} is not valid settings JSON — fix or move it, then retry`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Existing one-time backup, if tmux-ide already made one for this file. */
|
|
116
|
+
function existingBackup(settingsPath) {
|
|
117
|
+
const dir = dirname(settingsPath);
|
|
118
|
+
const prefix = `${basename(settingsPath)}${BACKUP_INFIX}`;
|
|
119
|
+
try {
|
|
120
|
+
const found = readdirSync(dir).find((name) => name.startsWith(prefix));
|
|
121
|
+
return found ? join(dir, found) : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function writeSettings(path, settings, exists) {
|
|
128
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
129
|
+
let backup = null;
|
|
130
|
+
if (exists) {
|
|
131
|
+
backup = existingBackup(path);
|
|
132
|
+
if (!backup) {
|
|
133
|
+
const stamp = new Date()
|
|
134
|
+
.toISOString()
|
|
135
|
+
.replace(/[-:]/gu, "")
|
|
136
|
+
.replace(/\.\d+Z$/u, "Z");
|
|
137
|
+
backup = `${path}${BACKUP_INFIX}${stamp}`;
|
|
138
|
+
copyFileSync(path, backup);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const temporary = join(dirname(path), `.${basename(path)}.tmux-ide-${process.pid}.tmp`);
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
144
|
+
if (exists)
|
|
145
|
+
chmodSync(temporary, statSync(path).mode & 0o777);
|
|
146
|
+
renameSync(temporary, path);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
try {
|
|
150
|
+
unlinkSync(temporary);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* already renamed or never written */
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return backup;
|
|
158
|
+
}
|
|
159
|
+
function detected(paths, onPath) {
|
|
160
|
+
return existsSync(paths.claudeDir) || onPath();
|
|
161
|
+
}
|
|
162
|
+
function guarded(paths) {
|
|
163
|
+
if (!paths && resolveRuntimeNamespace().development)
|
|
164
|
+
throw new Error("Development agent-teams changes require explicit fixture paths");
|
|
165
|
+
const resolved = paths ?? agentTeamsPaths();
|
|
166
|
+
runtimeOwnedPath(resolved.settingsPath);
|
|
167
|
+
return resolved;
|
|
168
|
+
}
|
|
169
|
+
/** Enable agent teams when Claude Code is installed and the user hasn't opted out. */
|
|
170
|
+
export function enableClaudeAgentTeams(paths, onPath = () => claudeOnPath(), { overrideOptOut = false } = {}) {
|
|
171
|
+
const resolved = guarded(paths);
|
|
172
|
+
const { settingsPath } = resolved;
|
|
173
|
+
const base = { settingsPath, changes: [], backup: null };
|
|
174
|
+
if (!detected(resolved, onPath))
|
|
175
|
+
return {
|
|
176
|
+
...base,
|
|
177
|
+
action: "skipped",
|
|
178
|
+
claudeDetected: false,
|
|
179
|
+
message: `Claude Code not found (no ${resolved.claudeDir}, no claude on PATH); agent teams not configured.`,
|
|
180
|
+
};
|
|
181
|
+
let read;
|
|
182
|
+
try {
|
|
183
|
+
read = readSettings(settingsPath);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return { ...base, action: "invalid", claudeDetected: true, message: error.message };
|
|
187
|
+
}
|
|
188
|
+
const merged = mergeAgentTeams(read.settings, { overrideOptOut });
|
|
189
|
+
if (merged.setting === "disabled-by-user")
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
action: "disabled-by-user",
|
|
193
|
+
claudeDetected: true,
|
|
194
|
+
message: `Claude Code agent teams are disabled by user in ${settingsPath} (${AGENT_TEAMS_ENV}); ` +
|
|
195
|
+
"left unchanged. Enable explicitly: tmux-ide integration agent-teams enable",
|
|
196
|
+
};
|
|
197
|
+
if (merged.changes.length === 0)
|
|
198
|
+
return {
|
|
199
|
+
...base,
|
|
200
|
+
action: "unchanged",
|
|
201
|
+
claudeDetected: true,
|
|
202
|
+
message: `Claude Code agent teams already enabled in ${settingsPath}.`,
|
|
203
|
+
};
|
|
204
|
+
const backup = writeSettings(settingsPath, merged.settings, read.exists);
|
|
205
|
+
return {
|
|
206
|
+
settingsPath,
|
|
207
|
+
changes: merged.changes,
|
|
208
|
+
backup,
|
|
209
|
+
action: "enabled",
|
|
210
|
+
claudeDetected: true,
|
|
211
|
+
message: `Enabled Claude Code agent teams in ${settingsPath} (${merged.changes.join(", ")}` +
|
|
212
|
+
`${backup ? `; backup: ${backup}` : ""}). Disable: tmux-ide integration agent-teams disable`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Record the user's opt-out so neither the installer nor `enable` turns it back on silently. */
|
|
216
|
+
export function disableClaudeAgentTeams(paths) {
|
|
217
|
+
const resolved = guarded(paths);
|
|
218
|
+
const { settingsPath } = resolved;
|
|
219
|
+
const base = { settingsPath, changes: [], backup: null, claudeDetected: true };
|
|
220
|
+
let read;
|
|
221
|
+
try {
|
|
222
|
+
read = readSettings(settingsPath);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return { ...base, action: "invalid", message: error.message };
|
|
226
|
+
}
|
|
227
|
+
const next = disableAgentTeams(read.settings);
|
|
228
|
+
if (next.changes.length === 0)
|
|
229
|
+
return {
|
|
230
|
+
...base,
|
|
231
|
+
action: "unchanged",
|
|
232
|
+
message: `Claude Code agent teams already disabled in ${settingsPath}.`,
|
|
233
|
+
};
|
|
234
|
+
const backup = writeSettings(settingsPath, next.settings, read.exists);
|
|
235
|
+
return {
|
|
236
|
+
...base,
|
|
237
|
+
changes: next.changes,
|
|
238
|
+
backup,
|
|
239
|
+
action: "disabled",
|
|
240
|
+
message: `Disabled Claude Code agent teams in ${settingsPath} (${next.changes.join(", ")}` +
|
|
241
|
+
`${backup ? `; backup: ${backup}` : ""}). Re-enable: tmux-ide integration agent-teams enable`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export function claudeAgentTeamsStatus(paths = agentTeamsPaths(), onPath = () => claudeOnPath()) {
|
|
245
|
+
const claudeDetected = detected(paths, onPath);
|
|
246
|
+
try {
|
|
247
|
+
const { settings } = readSettings(paths.settingsPath);
|
|
248
|
+
const mode = settings.teammateMode;
|
|
249
|
+
return {
|
|
250
|
+
settingsPath: paths.settingsPath,
|
|
251
|
+
claudeDetected,
|
|
252
|
+
setting: agentTeamsSetting(settings),
|
|
253
|
+
teammateMode: typeof mode === "string" ? mode : null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return {
|
|
258
|
+
settingsPath: paths.settingsPath,
|
|
259
|
+
claudeDetected,
|
|
260
|
+
setting: "invalid-settings",
|
|
261
|
+
teammateMode: null,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STOCK_CAPTURE_TAB_UNAVAILABLE } from "@tmux-ide/contracts";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { TerminalDeliveryAssembler, decodeVerifiedCompactSemanticTerminalUpdate, decodeVerifiedCompactSemanticTerminalUpdateCooperatively, decodeVerifiedLegacySemanticTerminalUpdate, terminalDeliveryEncodingAccepted, } from "@tmux-ide/core";
|
|
3
4
|
import { PaneStreamOperationError, } from "@tmux-ide/daemon-client/pane-stream-client";
|
|
@@ -193,7 +194,10 @@ function updateFromDelivery(envelope, payload, canonicalSnapshot, cols, rows) {
|
|
|
193
194
|
stateHash: envelope.canonicalStateHash,
|
|
194
195
|
hashAlgorithm: "fnv1a64-v1",
|
|
195
196
|
};
|
|
196
|
-
|
|
197
|
+
// Wire patches may span skipped revisions; ordinary canonical patches may not.
|
|
198
|
+
// Publish the fully verified resulting state at its actual revision.
|
|
199
|
+
if (payload.frame === "seed" ||
|
|
200
|
+
(payload.frame === "patch" && payload.revision > payload.baseRevision + 1)) {
|
|
197
201
|
return Object.freeze({
|
|
198
202
|
...common,
|
|
199
203
|
type: "terminal.seed",
|
|
@@ -245,6 +249,7 @@ class WireTerminalEndpoint {
|
|
|
245
249
|
#resolveReady;
|
|
246
250
|
#readySettled = false;
|
|
247
251
|
#hasCanonicalSeed = false;
|
|
252
|
+
#unavailable = null;
|
|
248
253
|
constructor(options) {
|
|
249
254
|
this.#workspaceName = options.workspaceName;
|
|
250
255
|
this.#semanticPaneId = options.semanticPaneId;
|
|
@@ -272,8 +277,11 @@ class WireTerminalEndpoint {
|
|
|
272
277
|
this.#readySettled = true;
|
|
273
278
|
this.#resolveReady(true);
|
|
274
279
|
}
|
|
280
|
+
get settled() {
|
|
281
|
+
return this.#unavailable !== null || this.inputReady;
|
|
282
|
+
}
|
|
275
283
|
get inputReady() {
|
|
276
|
-
return !this.#closed && this.#hasCanonicalSeed && !this.#reseedRequired;
|
|
284
|
+
return (!this.#closed && this.#unavailable === null && this.#hasCanonicalSeed && !this.#reseedRequired);
|
|
277
285
|
}
|
|
278
286
|
async subscription() {
|
|
279
287
|
const ready = await this.#ready;
|
|
@@ -291,6 +299,8 @@ class WireTerminalEndpoint {
|
|
|
291
299
|
this.#subscription = null;
|
|
292
300
|
});
|
|
293
301
|
this.#subscription = subscription;
|
|
302
|
+
if (this.#unavailable)
|
|
303
|
+
subscription.unavailable(this.#unavailable);
|
|
294
304
|
return subscription;
|
|
295
305
|
}
|
|
296
306
|
accept(message) {
|
|
@@ -301,10 +311,30 @@ class WireTerminalEndpoint {
|
|
|
301
311
|
return;
|
|
302
312
|
}
|
|
303
313
|
if (message.type === "terminal.delivery.fault") {
|
|
314
|
+
if (message.deliveryNonce !== this.#negotiated.deliveryNonce) {
|
|
315
|
+
this.#failConnection(new Error("Terminal fault nonce does not match negotiation"));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// The exact shared diagnostic is currently the compatibility-preserving discriminator.
|
|
319
|
+
if (message.reason === "source-closed" && message.message === STOCK_CAPTURE_TAB_UNAVAILABLE) {
|
|
320
|
+
this.#unavailable = message.message;
|
|
321
|
+
this.#decodeToken = null;
|
|
322
|
+
this.#assembler = null;
|
|
323
|
+
this.#assemblyStorage = null;
|
|
324
|
+
this.#envelope = null;
|
|
325
|
+
this.#pending = null;
|
|
326
|
+
this.#canonicalSnapshot = null;
|
|
327
|
+
this.#hasCanonicalSeed = false;
|
|
328
|
+
this.#subscription?.unavailable(message.message);
|
|
329
|
+
this.#canonicalSeedReady();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
304
332
|
const detail = message.message.trim();
|
|
305
333
|
this.#failConnection(new Error(`Terminal delivery failed for ${this.#semanticPaneId}: ${message.reason}${detail ? ` (${detail})` : ""}`));
|
|
306
334
|
return;
|
|
307
335
|
}
|
|
336
|
+
if (this.#unavailable)
|
|
337
|
+
return;
|
|
308
338
|
if (message.type === "terminal.delivery") {
|
|
309
339
|
this.#acceptEnvelope(message);
|
|
310
340
|
return;
|
|
@@ -601,6 +631,35 @@ class WireTerminalSubscription {
|
|
|
601
631
|
#didClose;
|
|
602
632
|
frozen = false;
|
|
603
633
|
closed = false;
|
|
634
|
+
#unavailable = null;
|
|
635
|
+
#unavailableListeners = new Set();
|
|
636
|
+
onUnavailable(listener) {
|
|
637
|
+
if (this.closed)
|
|
638
|
+
return () => { };
|
|
639
|
+
this.#unavailableListeners.add(listener);
|
|
640
|
+
if (this.#unavailable) {
|
|
641
|
+
try {
|
|
642
|
+
listener(this.#unavailable);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return () => this.#unavailableListeners.delete(listener);
|
|
649
|
+
}
|
|
650
|
+
unavailable(message) {
|
|
651
|
+
if (this.closed)
|
|
652
|
+
return;
|
|
653
|
+
this.#unavailable = message;
|
|
654
|
+
for (const listener of [...this.#unavailableListeners]) {
|
|
655
|
+
try {
|
|
656
|
+
listener(message);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
604
663
|
constructor(generation, flush, didClose) {
|
|
605
664
|
this.generation = generation;
|
|
606
665
|
this.#flush = flush;
|
|
@@ -635,6 +694,7 @@ class WireTerminalSubscription {
|
|
|
635
694
|
return;
|
|
636
695
|
this.closed = true;
|
|
637
696
|
this.#listeners.clear();
|
|
697
|
+
this.#unavailableListeners.clear();
|
|
638
698
|
this.#didClose();
|
|
639
699
|
}
|
|
640
700
|
}
|
|
@@ -1170,7 +1230,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1170
1230
|
});
|
|
1171
1231
|
return request.result;
|
|
1172
1232
|
}
|
|
1173
|
-
if ([...endpoints.values()].some((endpoint) => !endpoint.
|
|
1233
|
+
if ([...endpoints.values()].some((endpoint) => !endpoint.settled))
|
|
1174
1234
|
return Promise.resolve("geometry-authority-conflict");
|
|
1175
1235
|
return (semanticWindowId === undefined
|
|
1176
1236
|
? opened.fitViewport(cols, rows)
|
|
@@ -1219,7 +1279,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1219
1279
|
// until a different topology may be current; fail closed during seeding.
|
|
1220
1280
|
if (closed ||
|
|
1221
1281
|
!latestLayoutSnapshot.windowLinks ||
|
|
1222
|
-
[...endpoints.values()].some((endpoint) => !endpoint.
|
|
1282
|
+
[...endpoints.values()].some((endpoint) => !endpoint.settled))
|
|
1223
1283
|
throw new Error("Terminal inventory is still receiving canonical state");
|
|
1224
1284
|
return (await opened.submitIntent(operationId, intent)) ?? undefined;
|
|
1225
1285
|
},
|
|
@@ -496,7 +496,16 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
496
496
|
}
|
|
497
497
|
if (this._needsWalk) {
|
|
498
498
|
this._needsWalk = false;
|
|
499
|
-
|
|
499
|
+
try {
|
|
500
|
+
this.walk();
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
// The source may have consumed its dirty rows before a grapheme or
|
|
504
|
+
// presentation pass fails. Rebuild the retained surface on retry.
|
|
505
|
+
this._needsWalk = true;
|
|
506
|
+
this._forceFull = true;
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
500
509
|
}
|
|
501
510
|
else if (this._needsCursorPresentation) {
|
|
502
511
|
this.applyCursorOnlyPresentation();
|
|
@@ -154,13 +154,13 @@ export function createReferenceTraceWriter(stream, options = {}) {
|
|
|
154
154
|
firstDroppedRecord ??= droppedRecordKind(value);
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
|
-
const line = `${JSON.stringify(value)}\n`;
|
|
158
|
-
const bytes = Buffer.byteLength(line);
|
|
159
|
-
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
160
|
-
oversizedRecords += 1;
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
157
|
try {
|
|
158
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
159
|
+
const bytes = Buffer.byteLength(line);
|
|
160
|
+
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
161
|
+
oversizedRecords += 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
164
|
if (!saturated && pendingCount() === 0) {
|
|
165
165
|
writeLine(line);
|
|
166
166
|
return;
|
|
@@ -233,7 +233,15 @@ export function createReferenceTraceWriter(stream, options = {}) {
|
|
|
233
233
|
return Object.freeze({ append, appendCritical, snapshot, close });
|
|
234
234
|
}
|
|
235
235
|
function droppedRecordKind(value) {
|
|
236
|
-
const field = (key) =>
|
|
236
|
+
const field = (key) => {
|
|
237
|
+
try {
|
|
238
|
+
const result = value[key];
|
|
239
|
+
return typeof result === "string" ? result.slice(0, 64) : null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
237
245
|
return Object.freeze({
|
|
238
246
|
type: field("type"),
|
|
239
247
|
stage: field("stage"),
|
|
@@ -258,7 +266,12 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
258
266
|
const inputs = new Map();
|
|
259
267
|
let droppedInputs = 0;
|
|
260
268
|
let closed = false;
|
|
261
|
-
const
|
|
269
|
+
const attempts = { begun: 0, completed: 0, superseded: 0, expired: 0, cancelled: 0 };
|
|
270
|
+
const snapshot = () => Object.freeze({
|
|
271
|
+
pendingInputs: inputs.size,
|
|
272
|
+
droppedInputs,
|
|
273
|
+
inputAttempts: Object.freeze({ ...attempts, pending: inputs.size }),
|
|
274
|
+
});
|
|
262
275
|
return Object.freeze({
|
|
263
276
|
detailedWindowPresentationFrames: detailed ? true : undefined,
|
|
264
277
|
frame: (intervalMs, window) => {
|
|
@@ -464,67 +477,79 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
464
477
|
}
|
|
465
478
|
: {}),
|
|
466
479
|
beginTerminalInput: (origin) => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
480
|
+
if (closed)
|
|
481
|
+
return Object.freeze({ traceId: "", finish: () => { }, cancel: () => { } });
|
|
482
|
+
attempts.begun += 1;
|
|
483
|
+
let traceId;
|
|
484
|
+
try {
|
|
485
|
+
const startedAtMicros = nowMicros();
|
|
486
|
+
attempts.expired += expireInputs(inputs, startedAtMicros);
|
|
487
|
+
// The transport carries one latest-only performance trace id. Once a
|
|
488
|
+
// newer input is admitted, an older probe can no longer be attributed to
|
|
489
|
+
// the exact changed-cell paint without lying. Retire it as superseded;
|
|
490
|
+
// `droppedInputs` is reserved for actual capacity loss.
|
|
491
|
+
attempts.superseded += inputs.size;
|
|
492
|
+
inputs.clear();
|
|
493
|
+
while (inputs.size >= MAX_PENDING_INPUTS) {
|
|
494
|
+
inputs.delete(inputs.keys().next().value);
|
|
495
|
+
droppedInputs += 1;
|
|
496
|
+
attempts.cancelled += 1;
|
|
497
|
+
}
|
|
498
|
+
traceId = createTraceId();
|
|
499
|
+
if ((detailed || options.inputOrigin) &&
|
|
500
|
+
origin &&
|
|
501
|
+
!closed &&
|
|
502
|
+
typeof options.inputFingerprintKey === "string" &&
|
|
503
|
+
options.inputFingerprintKey.length >= 32) {
|
|
504
|
+
const payload = Buffer.from(origin.payload);
|
|
505
|
+
const event = {
|
|
506
|
+
processId,
|
|
507
|
+
clockId: "opentui-performance-now",
|
|
508
|
+
clockKind: "performance-now",
|
|
509
|
+
atMicros: Number.isSafeInteger(origin.ingressAtMicros) &&
|
|
510
|
+
origin.ingressAtMicros <= startedAtMicros
|
|
511
|
+
? origin.ingressAtMicros
|
|
512
|
+
: startedAtMicros,
|
|
513
|
+
origin: origin.origin,
|
|
514
|
+
payloadByteCount: payload.byteLength,
|
|
515
|
+
payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
|
|
516
|
+
.update(traceId)
|
|
517
|
+
.update("\0")
|
|
518
|
+
.update(payload)
|
|
519
|
+
.digest("hex"),
|
|
520
|
+
parserConsumption: origin.origin === "keyboard"
|
|
521
|
+
? "keyboard-event"
|
|
522
|
+
: origin.origin === "bracketed-paste"
|
|
523
|
+
? "paste-event"
|
|
524
|
+
: "pointer-event",
|
|
525
|
+
...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
|
|
526
|
+
...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
|
|
527
|
+
...(Number.isSafeInteger(origin.pointerColumn)
|
|
528
|
+
? { pointerColumn: origin.pointerColumn }
|
|
529
|
+
: {}),
|
|
530
|
+
...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
|
|
531
|
+
...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
|
|
532
|
+
? { pointerButton: origin.pointerButton }
|
|
533
|
+
: {}),
|
|
534
|
+
traceId,
|
|
535
|
+
semanticPaneId: origin.semanticPaneId,
|
|
536
|
+
generation: origin.generation,
|
|
537
|
+
incarnation: origin.incarnation,
|
|
538
|
+
revision: origin.revision,
|
|
539
|
+
stateHash: origin.stateHash,
|
|
540
|
+
};
|
|
541
|
+
options.append({ version: 1, type: "performance.input-origin", ...event });
|
|
542
|
+
}
|
|
543
|
+
inputs.set(traceId, {
|
|
544
|
+
startedAtMicros,
|
|
545
|
+
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
546
|
+
endedAtMicros: null,
|
|
547
|
+
});
|
|
477
548
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
!closed &&
|
|
482
|
-
typeof options.inputFingerprintKey === "string" &&
|
|
483
|
-
options.inputFingerprintKey.length >= 32) {
|
|
484
|
-
const payload = Buffer.from(origin.payload);
|
|
485
|
-
const event = {
|
|
486
|
-
processId,
|
|
487
|
-
clockId: "opentui-performance-now",
|
|
488
|
-
clockKind: "performance-now",
|
|
489
|
-
atMicros: Number.isSafeInteger(origin.ingressAtMicros) &&
|
|
490
|
-
origin.ingressAtMicros <= startedAtMicros
|
|
491
|
-
? origin.ingressAtMicros
|
|
492
|
-
: startedAtMicros,
|
|
493
|
-
origin: origin.origin,
|
|
494
|
-
payloadByteCount: payload.byteLength,
|
|
495
|
-
payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
|
|
496
|
-
.update(traceId)
|
|
497
|
-
.update("\0")
|
|
498
|
-
.update(payload)
|
|
499
|
-
.digest("hex"),
|
|
500
|
-
parserConsumption: origin.origin === "keyboard"
|
|
501
|
-
? "keyboard-event"
|
|
502
|
-
: origin.origin === "bracketed-paste"
|
|
503
|
-
? "paste-event"
|
|
504
|
-
: "pointer-event",
|
|
505
|
-
...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
|
|
506
|
-
...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
|
|
507
|
-
...(Number.isSafeInteger(origin.pointerColumn)
|
|
508
|
-
? { pointerColumn: origin.pointerColumn }
|
|
509
|
-
: {}),
|
|
510
|
-
...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
|
|
511
|
-
...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
|
|
512
|
-
? { pointerButton: origin.pointerButton }
|
|
513
|
-
: {}),
|
|
514
|
-
traceId,
|
|
515
|
-
semanticPaneId: origin.semanticPaneId,
|
|
516
|
-
generation: origin.generation,
|
|
517
|
-
incarnation: origin.incarnation,
|
|
518
|
-
revision: origin.revision,
|
|
519
|
-
stateHash: origin.stateHash,
|
|
520
|
-
};
|
|
521
|
-
options.append({ version: 1, type: "performance.input-origin", ...event });
|
|
549
|
+
catch (error) {
|
|
550
|
+
attempts.cancelled += 1;
|
|
551
|
+
throw error;
|
|
522
552
|
}
|
|
523
|
-
inputs.set(traceId, {
|
|
524
|
-
startedAtMicros,
|
|
525
|
-
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
526
|
-
endedAtMicros: null,
|
|
527
|
-
});
|
|
528
553
|
let finished = false;
|
|
529
554
|
return Object.freeze({
|
|
530
555
|
traceId,
|
|
@@ -536,10 +561,19 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
536
561
|
if (input)
|
|
537
562
|
input.endedAtMicros = nowMicros();
|
|
538
563
|
},
|
|
539
|
-
cancel: () =>
|
|
564
|
+
cancel: () => {
|
|
565
|
+
if (inputs.delete(traceId))
|
|
566
|
+
attempts.cancelled += 1;
|
|
567
|
+
},
|
|
540
568
|
});
|
|
541
569
|
},
|
|
542
|
-
terminalTraceSpan: (paint) =>
|
|
570
|
+
terminalTraceSpan: (paint) => {
|
|
571
|
+
if (closed || paint.processId !== processId)
|
|
572
|
+
return;
|
|
573
|
+
const outcome = recordCompletedTrace(inputs, paint, options.append);
|
|
574
|
+
if (outcome)
|
|
575
|
+
attempts[outcome] += 1;
|
|
576
|
+
},
|
|
543
577
|
terminalTraceStage: (event) => {
|
|
544
578
|
if ((detailed || options.inputDetail) && !closed)
|
|
545
579
|
options.append({ version: 1, type: "performance.stage", ...event });
|
|
@@ -560,6 +594,7 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
560
594
|
// Shutdown cancels probes that can no longer observe a future paint.
|
|
561
595
|
// They are not backpressure loss and must not survive as phantom work in
|
|
562
596
|
// a closed collector summary.
|
|
597
|
+
attempts.cancelled += inputs.size;
|
|
563
598
|
inputs.clear();
|
|
564
599
|
return snapshot();
|
|
565
600
|
},
|
|
@@ -568,10 +603,17 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
568
603
|
function recordCompletedTrace(inputs, paint, appendRecord) {
|
|
569
604
|
const input = inputs.get(paint.traceId);
|
|
570
605
|
if (!input || input.endedAtMicros === null)
|
|
571
|
-
return;
|
|
606
|
+
return null;
|
|
607
|
+
if (paint.clockId !== "opentui-performance-now" ||
|
|
608
|
+
paint.clockKind !== "performance-now" ||
|
|
609
|
+
!Number.isFinite(paint.startedAtMicros) ||
|
|
610
|
+
!Number.isFinite(paint.endedAtMicros) ||
|
|
611
|
+
paint.startedAtMicros < input.endedAtMicros ||
|
|
612
|
+
paint.endedAtMicros < paint.startedAtMicros)
|
|
613
|
+
return null;
|
|
572
614
|
if (paint.startedAtMicros > input.expiresAtMicros) {
|
|
573
615
|
inputs.delete(paint.traceId);
|
|
574
|
-
return;
|
|
616
|
+
return "expired";
|
|
575
617
|
}
|
|
576
618
|
inputs.delete(paint.traceId);
|
|
577
619
|
appendRecord({
|
|
@@ -588,11 +630,15 @@ function recordCompletedTrace(inputs, paint, appendRecord) {
|
|
|
588
630
|
authority: { generation: paint.generation, incarnation: paint.incarnation },
|
|
589
631
|
});
|
|
590
632
|
appendRecord({ version: 1, type: "performance.stage", ...paint });
|
|
633
|
+
return "completed";
|
|
591
634
|
}
|
|
592
635
|
function expireInputs(inputs, nowMicros) {
|
|
636
|
+
let expired = 0;
|
|
593
637
|
for (const [traceId, input] of inputs) {
|
|
594
638
|
if (input.expiresAtMicros > nowMicros)
|
|
595
639
|
break;
|
|
596
640
|
inputs.delete(traceId);
|
|
641
|
+
expired += 1;
|
|
597
642
|
}
|
|
643
|
+
return expired;
|
|
598
644
|
}
|