tmux-ide 2.9.0-beta.20 → 2.9.0-beta.21
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/bin/cli.js +100 -8
- package/package.json +3 -2
- package/packages/daemon/dist/command-center/diagnostics.js +65 -0
- package/packages/daemon/dist/command-center/log-stream.js +9 -0
- package/packages/daemon/dist/command-center/server.js +7 -0
- package/packages/daemon/dist/lib/app-config.js +4 -2
- package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
- package/packages/daemon/dist/lib/soak-verdict.js +185 -35
- package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
- package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
- package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
- package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
- package/packages/daemon/dist/tui/mirror/theme.js +4 -31
- package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
- package/packages/daemon/src/command-center/diagnostics.ts +75 -0
- package/packages/daemon/src/command-center/log-stream.ts +8 -0
- package/packages/daemon/src/command-center/server.ts +8 -0
- package/packages/daemon/src/lib/app-config.ts +11 -3
- package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
- package/packages/daemon/src/lib/soak-verdict.ts +314 -23
- package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
- package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
- package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
- package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +9 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
- package/packages/daemon/src/tui/mirror/theme.ts +4 -39
- package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Pure analysis for the daemon soak harness (`packages/daemon/scripts/soak-daemon.mjs`).
|
|
3
|
-
*
|
|
4
|
-
* The harness records one JSONL sample per interval; this module turns the
|
|
5
|
-
* sample series into a summary and a verdict against explicit thresholds. It
|
|
6
|
-
* has no io and no dependencies so the harness can import it directly and the
|
|
7
|
-
* numbers it prints are the numbers the colocated tests pin down.
|
|
8
|
-
*/
|
|
1
|
+
import { MEMORY_KEYS, RESOURCE_KEYS, } from "./soak-diagnostics.js";
|
|
9
2
|
export const DEFAULT_SOAK_THRESHOLDS = {
|
|
10
3
|
maxRssGrowthMiBPerHour: 8,
|
|
11
4
|
maxFdDrift: 16,
|
|
@@ -56,12 +49,25 @@ export function percentile(values, fraction) {
|
|
|
56
49
|
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
|
|
57
50
|
return sorted[rank];
|
|
58
51
|
}
|
|
52
|
+
/** At most 228 buckets for probes capped at 60 seconds, plus an overflow bucket. */
|
|
59
53
|
export function percentiles(values) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
const buckets = {};
|
|
55
|
+
let max = null;
|
|
56
|
+
for (const value of values) {
|
|
57
|
+
if (!Number.isFinite(value) || value < 0)
|
|
58
|
+
continue;
|
|
59
|
+
max = Math.max(max ?? 0, value);
|
|
60
|
+
const index = value <= 1 ? 0 : Math.min(227, Math.ceil(Math.log(value) / Math.log(1.05)));
|
|
61
|
+
buckets[index] = (buckets[index] ?? 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
return mergePercentiles([
|
|
64
|
+
{
|
|
65
|
+
count: Object.values(buckets).reduce((a, b) => a + b, 0),
|
|
66
|
+
p50: null,
|
|
67
|
+
max,
|
|
68
|
+
buckets,
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
65
71
|
}
|
|
66
72
|
/**
|
|
67
73
|
* Fit a per-sample metric against elapsed time and report the slope per hour.
|
|
@@ -105,25 +111,83 @@ function maxValue(samples, pick) {
|
|
|
105
111
|
}
|
|
106
112
|
return max;
|
|
107
113
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Merge per-interval percentile records into a whole-run estimate. Exact
|
|
110
|
-
* per-sample series are not retained across intervals, so the p50 here is the
|
|
111
|
-
* count-weighted median of interval medians and the max is the true max.
|
|
112
|
-
*/
|
|
114
|
+
/** Merge bounded histograms, never a median of medians. Legacy p50 is unmeasured. */
|
|
113
115
|
export function mergePercentiles(records) {
|
|
114
|
-
const
|
|
115
|
-
let max = null;
|
|
116
|
+
const buckets = {};
|
|
116
117
|
let count = 0;
|
|
118
|
+
let max = null;
|
|
119
|
+
let missing = false;
|
|
117
120
|
for (const record of records) {
|
|
118
|
-
if (record.p50 === null || record.count === 0)
|
|
119
|
-
continue;
|
|
120
121
|
count += record.count;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (record.max !== null
|
|
124
|
-
max = record.max;
|
|
122
|
+
if (record.count && !record.buckets)
|
|
123
|
+
missing = true;
|
|
124
|
+
if (record.max !== null)
|
|
125
|
+
max = Math.max(max ?? 0, record.max);
|
|
126
|
+
for (const [key, value] of Object.entries(record.buckets ?? {}))
|
|
127
|
+
buckets[key] = (buckets[key] ?? 0) + value;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
let cumulative = 0;
|
|
130
|
+
let p50 = null;
|
|
131
|
+
if (!missing && count) {
|
|
132
|
+
for (const key of Object.keys(buckets)
|
|
133
|
+
.map(Number)
|
|
134
|
+
.sort((a, b) => a - b)) {
|
|
135
|
+
cumulative += buckets[key];
|
|
136
|
+
if (cumulative >= Math.ceil(count / 2)) {
|
|
137
|
+
p50 = key === 227 ? max : Math.min(max ?? Infinity, 1.05 ** key);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { count, p50, max, buckets };
|
|
143
|
+
}
|
|
144
|
+
export function soakTrends(samples, warmupSeconds, trailingSeconds) {
|
|
145
|
+
const end = samples.at(-1)?.elapsedSeconds ?? 0;
|
|
146
|
+
const fit = (window) => ({
|
|
147
|
+
memoryMiBPerHour: Object.fromEntries(MEMORY_KEYS.map((key) => [
|
|
148
|
+
key,
|
|
149
|
+
growthPerHour(window, (s) => {
|
|
150
|
+
const value = s.diagnostics?.sample?.memory[key];
|
|
151
|
+
return value === undefined ? null : value / 1024 ** 2;
|
|
152
|
+
}),
|
|
153
|
+
])),
|
|
154
|
+
activeResourcesPerHour: Object.fromEntries(RESOURCE_KEYS.map((key) => [
|
|
155
|
+
key,
|
|
156
|
+
growthPerHour(window, (s) => s.diagnostics?.sample?.activeResources?.[key] ?? null),
|
|
157
|
+
])),
|
|
158
|
+
diagnosticCpuPercentPerHour: growthPerHour(window, (s) => s.diagnosticDelta?.status === "ok" ? s.diagnosticDelta.cpuPercent : null),
|
|
159
|
+
eventLoopUtilizationPerHour: growthPerHour(window, (s) => s.diagnosticDelta?.status === "ok" ? s.diagnosticDelta.eventLoopUtilization : null),
|
|
160
|
+
rssMiBPerHour: growthPerHour(window, (s) => (s.rssKiB === null ? null : s.rssKiB / 1024)),
|
|
161
|
+
cpuPercentPerHour: growthPerHour(window, (s) => s.cpuDeltaSeconds === null || s.intervalSeconds <= 0
|
|
162
|
+
? null
|
|
163
|
+
: (100 * s.cpuDeltaSeconds) / s.intervalSeconds),
|
|
164
|
+
spawnsPerMinutePerHour: growthPerHour(window, (s) => s.intervalSeconds <= 0 ? null : (60 * s.tmuxSpawns) / s.intervalSeconds),
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
whole: fit(samples),
|
|
168
|
+
afterWarmup: fit(samples.filter((s) => s.elapsedSeconds >= warmupSeconds)),
|
|
169
|
+
trailing: fit(samples.filter((s) => s.elapsedSeconds >= Math.max(warmupSeconds, end - trailingSeconds))),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Descriptive only: heap changes can reflect GC and do not prove retention. */
|
|
173
|
+
export function summarizeDiagnostics(samples) {
|
|
174
|
+
const describe = (pick) => ({
|
|
175
|
+
start: firstValue(samples, pick),
|
|
176
|
+
end: lastValue(samples, pick),
|
|
177
|
+
max: maxValue(samples, pick),
|
|
178
|
+
measuredSamples: samples.filter((s) => pick(s) !== null).length,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
validSamples: samples.filter((s) => s.diagnostics?.status === "ok").length,
|
|
182
|
+
unsupportedResourceSamples: samples.filter((s) => s.diagnostics?.status === "ok" && s.diagnostics.sample.activeResources === null).length,
|
|
183
|
+
memoryBytes: Object.fromEntries(MEMORY_KEYS.map((key) => [key, describe((s) => s.diagnostics?.sample?.memory[key] ?? null)])),
|
|
184
|
+
activeResources: Object.fromEntries(RESOURCE_KEYS.map((key) => [
|
|
185
|
+
key,
|
|
186
|
+
describe((s) => s.diagnostics?.sample?.activeResources?.[key] ?? null),
|
|
187
|
+
])),
|
|
188
|
+
cpuPercent: describe((s) => s.diagnosticDelta?.status === "ok" ? s.diagnosticDelta.cpuPercent : null),
|
|
189
|
+
eventLoopUtilization: describe((s) => s.diagnosticDelta?.status === "ok" ? s.diagnosticDelta.eventLoopUtilization : null),
|
|
190
|
+
};
|
|
127
191
|
}
|
|
128
192
|
export function summarizeSoak(samples) {
|
|
129
193
|
const rss = (sample) => sample.rssKiB === null ? null : sample.rssKiB / KIB_PER_MIB;
|
|
@@ -149,6 +213,7 @@ export function summarizeSoak(samples) {
|
|
|
149
213
|
disconnects += sample.unexpectedDisconnects;
|
|
150
214
|
}
|
|
151
215
|
return {
|
|
216
|
+
diagnostics: summarizeDiagnostics(samples),
|
|
152
217
|
samples: samples.length,
|
|
153
218
|
durationSeconds,
|
|
154
219
|
rssStartMiB: firstValue(samples, rss),
|
|
@@ -183,9 +248,12 @@ function boundCheck(id, observed, bound, detail, options = {}) {
|
|
|
183
248
|
* measure it (too few samples for a fit, a metric never collected); the
|
|
184
249
|
* overall verdict is then `inconclusive` unless some other check failed.
|
|
185
250
|
*/
|
|
186
|
-
export function evaluateSoak(samples, thresholds = DEFAULT_SOAK_THRESHOLDS) {
|
|
251
|
+
export function evaluateSoak(samples, thresholds = DEFAULT_SOAK_THRESHOLDS, evidence) {
|
|
187
252
|
const summary = summarizeSoak(samples);
|
|
188
|
-
const enoughForFit = samples.length >= thresholds.minSamplesForFit
|
|
253
|
+
const enoughForFit = samples.length >= thresholds.minSamplesForFit &&
|
|
254
|
+
(!evidence ||
|
|
255
|
+
samples.filter((sample) => sample.elapsedSeconds >= evidence.warmupSeconds).length >=
|
|
256
|
+
thresholds.minSamplesForFit);
|
|
189
257
|
const checks = [
|
|
190
258
|
enoughForFit
|
|
191
259
|
? boundCheck("rss-growth", summary.rssGrowthMiBPerHour, thresholds.maxRssGrowthMiBPerHour, `fitted RSS slope MiB/h over ${samples.length} samples (r2 ${summary.rssFitR2?.toFixed(2) ?? "n/a"})`)
|
|
@@ -194,7 +262,7 @@ export function evaluateSoak(samples, thresholds = DEFAULT_SOAK_THRESHOLDS) {
|
|
|
194
262
|
ok: null,
|
|
195
263
|
observed: summary.rssGrowthMiBPerHour,
|
|
196
264
|
bound: thresholds.maxRssGrowthMiBPerHour,
|
|
197
|
-
detail: `needs ${thresholds.minSamplesForFit} samples, have ${samples.length}`,
|
|
265
|
+
detail: `needs ${thresholds.minSamplesForFit} samples and, when declared, post-warmup coverage; have ${samples.length} total`,
|
|
198
266
|
},
|
|
199
267
|
boundCheck("fd-drift", summary.fdStart === null || summary.fdEnd === null ? null : summary.fdEnd - summary.fdStart, thresholds.maxFdDrift, "open fd count end minus start", { absolute: true }),
|
|
200
268
|
enoughForFit
|
|
@@ -204,15 +272,71 @@ export function evaluateSoak(samples, thresholds = DEFAULT_SOAK_THRESHOLDS) {
|
|
|
204
272
|
ok: null,
|
|
205
273
|
observed: summary.fdGrowthPerHour,
|
|
206
274
|
bound: thresholds.maxFdGrowthPerHour,
|
|
207
|
-
detail: `needs ${thresholds.minSamplesForFit} samples, have ${samples.length}`,
|
|
275
|
+
detail: `needs ${thresholds.minSamplesForFit} samples and, when declared, post-warmup coverage; have ${samples.length} total`,
|
|
208
276
|
},
|
|
209
|
-
boundCheck("ping-rtt-p50", summary.pingRttMs.p50, thresholds.maxPingRttP50Ms, "
|
|
210
|
-
boundCheck("receipt-latency-p50", summary.receiptLatencyMs.p50, thresholds.maxReceiptLatencyP50Ms, "wait agent-status receipt latency p50 ms after the flip"),
|
|
277
|
+
boundCheck("ping-rtt-p50", summary.pingRttMs.p50, thresholds.maxPingRttP50Ms, "WebSocket transport control RTT p50 upper bound ms (not semantic handler latency)"),
|
|
278
|
+
boundCheck("receipt-latency-p50", summary.receiptLatencyMs.p50, thresholds.maxReceiptLatencyP50Ms, "wait agent-status receipt latency p50 upper bound ms after the flip"),
|
|
211
279
|
boundCheck("observer-gaps", summary.observerGapWarnings, thresholds.maxObserverGapWarnings, "interaction observer gap warnings"),
|
|
212
280
|
boundCheck("daemon-restarts", summary.daemonRestarts, thresholds.maxDaemonRestarts, "daemon pid or instance changes"),
|
|
213
281
|
boundCheck("receipt-failures", summary.receiptFailures, thresholds.maxReceiptFailures, "receipt waiters that failed or timed out"),
|
|
214
282
|
boundCheck("unexpected-disconnects", summary.unexpectedDisconnects, thresholds.maxUnexpectedDisconnects, "events-client drops outside scheduled reconnects"),
|
|
215
283
|
];
|
|
284
|
+
const coverage = (id, complete, detail) => checks.push({
|
|
285
|
+
id,
|
|
286
|
+
ok: complete === true ? true : null,
|
|
287
|
+
observed: complete === true ? 1 : null,
|
|
288
|
+
bound: 1,
|
|
289
|
+
detail,
|
|
290
|
+
});
|
|
291
|
+
coverage("duration-complete", evidence?.completed && evidence.observedSeconds >= evidence.requestedSeconds, "requested duration reached without interruption");
|
|
292
|
+
coverage("telemetry-complete", evidence?.telemetryComplete &&
|
|
293
|
+
samples.every((s) => [s.rssKiB, s.cpuSeconds, s.openFds].every((value) => value !== null && Number.isFinite(value))), "all required samples and metrics present");
|
|
294
|
+
coverage("diagnostics-coverage", samples.length > 0 &&
|
|
295
|
+
samples.every((s) => s.diagnostics?.status === "ok" && s.diagnostics.sample.activeResources !== null), "owner diagnostics and supported resource counts required at every sample; missing/malformed/404 remain inconclusive");
|
|
296
|
+
coverage("diagnostics-deltas", samples.length > 1 &&
|
|
297
|
+
samples.every((s, i) => i === 0
|
|
298
|
+
? s.diagnosticDelta?.status === "missing-baseline"
|
|
299
|
+
: s.diagnosticDelta?.status === "ok" && s.diagnosticDelta.eventLoopUtilization !== null), "adjacent monotonic cumulative counters required after first baseline");
|
|
300
|
+
checks.push(boundCheck("diagnostics-identity", samples.filter((s) => s.diagnostics?.status === "identity-mismatch" ||
|
|
301
|
+
s.diagnosticDelta?.status === "identity-mismatch").length, 0, "diagnostics must match original daemon identity and PID"));
|
|
302
|
+
coverage("load-complete", evidence?.loadCompleted, "each declared workload completed at least once");
|
|
303
|
+
coverage("log-coverage", evidence?.logCoverageComplete, "continuous log stream, bookmark, no gap or replay ambiguity");
|
|
304
|
+
coverage("run-evidence", evidence !== undefined, "explicit run and teardown evidence supplied");
|
|
305
|
+
coverage("resource-trend-policy", evidence?.resourceTrendPolicyComplete, "heap/resources and CPU/spawn trend acceptance policy awaiting calibration and review");
|
|
306
|
+
if (evidence) {
|
|
307
|
+
const requiredFailures = [
|
|
308
|
+
"health",
|
|
309
|
+
"flip",
|
|
310
|
+
"promotion",
|
|
311
|
+
"send",
|
|
312
|
+
"receipt",
|
|
313
|
+
"daemonMissing",
|
|
314
|
+
"daemonExit",
|
|
315
|
+
"ack",
|
|
316
|
+
"pingDeadline",
|
|
317
|
+
"loop",
|
|
318
|
+
"sample",
|
|
319
|
+
];
|
|
320
|
+
for (const name of new Set([...requiredFailures, ...Object.keys(evidence.failures)])) {
|
|
321
|
+
const count = evidence.failures[name];
|
|
322
|
+
checks.push(boundCheck(name, typeof count === "number" && Number.isFinite(count) && count >= 0 ? count : null, 0, "explicit run failure count"));
|
|
323
|
+
}
|
|
324
|
+
coverage("reconnect-coverage", evidence.reconnectAttempts > 1 &&
|
|
325
|
+
evidence.reconnectAttempts === evidence.reconnectAcknowledged, "initial subscription and at least one reconnect acknowledged");
|
|
326
|
+
for (const [id, ok] of [
|
|
327
|
+
["shutdown-clean", evidence.shutdownClean],
|
|
328
|
+
["record-retired", evidence.recordRetired],
|
|
329
|
+
["cleanup-complete", evidence.cleanupComplete],
|
|
330
|
+
])
|
|
331
|
+
checks.push({ id, ok, observed: ok ? 0 : 1, bound: 0, detail: "observed teardown outcome" });
|
|
332
|
+
const trends = soakTrends(samples, evidence.warmupSeconds, evidence.trailingSeconds);
|
|
333
|
+
for (const [window, metrics] of Object.entries(trends)) {
|
|
334
|
+
if (window === "whole")
|
|
335
|
+
continue;
|
|
336
|
+
const fit = metrics.rssMiBPerHour;
|
|
337
|
+
checks.push(boundCheck(`rss-${window}`, fit && fit.points >= thresholds.minSamplesForFit ? fit.slope : null, thresholds.maxRssGrowthMiBPerHour, "predeclared window RSS slope MiB/h"));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
216
340
|
const failed = checks.some((check) => check.ok === false);
|
|
217
341
|
const unmeasured = checks.some((check) => check.ok === null);
|
|
218
342
|
return {
|
|
@@ -298,11 +422,23 @@ export function formatSoakReport(result) {
|
|
|
298
422
|
["tmux spawns", `${summary.tmuxSpawnsTotal} total ${num(summary.tmuxSpawnsPerMinute, 2)}/min`],
|
|
299
423
|
[
|
|
300
424
|
"ping rtt ms",
|
|
301
|
-
`p50 ${num(summary.pingRttMs.p50, 2)} max ${num(summary.pingRttMs.max, 2)} n ${summary.pingRttMs.count}`,
|
|
425
|
+
`p50 <= ${num(summary.pingRttMs.p50, 2)} max ${num(summary.pingRttMs.max, 2)} n ${summary.pingRttMs.count}`,
|
|
302
426
|
],
|
|
303
427
|
[
|
|
304
428
|
"receipt ms",
|
|
305
|
-
`p50 ${num(summary.receiptLatencyMs.p50, 0)} max ${num(summary.receiptLatencyMs.max, 0)} n ${summary.receiptLatencyMs.count}`,
|
|
429
|
+
`p50 <= ${num(summary.receiptLatencyMs.p50, 0)} max ${num(summary.receiptLatencyMs.max, 0)} n ${summary.receiptLatencyMs.count}`,
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
"owner diagnostics",
|
|
433
|
+
`${summary.diagnostics.validSamples}/${summary.samples} valid; resources unsupported ${summary.diagnostics.unsupportedResourceSamples}`,
|
|
434
|
+
],
|
|
435
|
+
[
|
|
436
|
+
"heap used MiB",
|
|
437
|
+
`start ${num(summary.diagnostics.memoryBytes.heapUsed?.start === null || summary.diagnostics.memoryBytes.heapUsed?.start === undefined ? null : summary.diagnostics.memoryBytes.heapUsed.start / 1024 ** 2)} end ${num(summary.diagnostics.memoryBytes.heapUsed?.end === null || summary.diagnostics.memoryBytes.heapUsed?.end === undefined ? null : summary.diagnostics.memoryBytes.heapUsed.end / 1024 ** 2)} (descriptive; GC-sensitive)`,
|
|
438
|
+
],
|
|
439
|
+
[
|
|
440
|
+
"diagnostic CPU",
|
|
441
|
+
`${num(summary.diagnostics.cpuPercent.start)} → ${num(summary.diagnostics.cpuPercent.end)} % interval; policy pending`,
|
|
306
442
|
],
|
|
307
443
|
["observer gaps", String(summary.observerGapWarnings)],
|
|
308
444
|
["daemon restarts", String(summary.daemonRestarts)],
|
|
@@ -320,3 +456,17 @@ export function formatSoakReport(result) {
|
|
|
320
456
|
lines.push(`verdict: ${result.verdict.toUpperCase()}`);
|
|
321
457
|
return lines.join("\n");
|
|
322
458
|
}
|
|
459
|
+
/** A revision acknowledges exactly the interest set sent in that subscribe. */
|
|
460
|
+
export function validSoakAck(frame, revision) {
|
|
461
|
+
if (!frame || typeof frame !== "object")
|
|
462
|
+
return false;
|
|
463
|
+
const ack = frame;
|
|
464
|
+
return (ack.type === "resource.interests-ack" &&
|
|
465
|
+
ack.interestRevision === revision &&
|
|
466
|
+
Array.isArray(ack.unavailableInterests) &&
|
|
467
|
+
ack.unavailableInterests.length === 0);
|
|
468
|
+
}
|
|
469
|
+
/** Unsolicited/stale control payloads cannot discharge the current probe. */
|
|
470
|
+
export function correlatedPongRtt(probe, payload, now) {
|
|
471
|
+
return payload === probe.id && Number.isFinite(now) && now >= probe.at ? now - probe.at : null;
|
|
472
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function color(red, green, blue) {
|
|
2
|
+
return { space: "srgb", red, green, blue, alpha: 255 };
|
|
3
|
+
}
|
|
4
|
+
export function parseTerminalHostColor(value) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return null;
|
|
7
|
+
const normalized = value.trim().toLowerCase();
|
|
8
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/u.exec(normalized)?.[1];
|
|
9
|
+
if (hex) {
|
|
10
|
+
const expanded = hex.length === 3 ? `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}` : hex;
|
|
11
|
+
return color(Number.parseInt(expanded.slice(0, 2), 16), Number.parseInt(expanded.slice(2, 4), 16), Number.parseInt(expanded.slice(4, 6), 16));
|
|
12
|
+
}
|
|
13
|
+
// OSC palette replies may use X11's rgb:RR/GG/BB form with one to four
|
|
14
|
+
// hexadecimal digits per channel. Scale each channel to a byte rather than
|
|
15
|
+
// truncating high-fidelity replies.
|
|
16
|
+
const x11 = /^rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})$/u.exec(normalized);
|
|
17
|
+
if (!x11)
|
|
18
|
+
return null;
|
|
19
|
+
const channel = (part) => {
|
|
20
|
+
const maximum = 16 ** part.length - 1;
|
|
21
|
+
return Math.round((Number.parseInt(part, 16) / maximum) * 255);
|
|
22
|
+
};
|
|
23
|
+
return color(channel(x11[1]), channel(x11[2]), channel(x11[3]));
|
|
24
|
+
}
|
|
25
|
+
/** Only a reported default background determines host appearance. */
|
|
26
|
+
export function terminalHostMode(background) {
|
|
27
|
+
const value = parseTerminalHostColor(background);
|
|
28
|
+
return value
|
|
29
|
+
? 0.299 * value.red + 0.587 * value.green + 0.114 * value.blue > 127.5
|
|
30
|
+
? "light"
|
|
31
|
+
: "dark"
|
|
32
|
+
: null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Quantized sRGB contrast; Oklab matrices: https://bottosson.github.io/posts/oklab/ . */
|
|
2
|
+
const LINEAR = Float64Array.from({ length: 256 }, (_, n) => {
|
|
3
|
+
const c = n / 255;
|
|
4
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
5
|
+
});
|
|
6
|
+
const luminance = (rgb) => 0.2126 * LINEAR[(rgb >>> 16) & 255] +
|
|
7
|
+
0.7152 * LINEAR[(rgb >>> 8) & 255] +
|
|
8
|
+
0.0722 * LINEAR[rgb & 255];
|
|
9
|
+
export function packedContrastRatio(a, b) {
|
|
10
|
+
const x = luminance(a) + 0.05, y = luminance(b) + 0.05;
|
|
11
|
+
return Math.max(x, y) / Math.min(x, y);
|
|
12
|
+
}
|
|
13
|
+
function byte(c) {
|
|
14
|
+
return Math.round(Math.max(0, Math.min(1, c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055)) * 255);
|
|
15
|
+
}
|
|
16
|
+
function candidate(L, a, b) {
|
|
17
|
+
// Reduce chroma along the same hue ray until inside sRGB, then quantize.
|
|
18
|
+
let lo = 0, hi = 1, result = -1;
|
|
19
|
+
for (let i = 0; i < 10; i++) {
|
|
20
|
+
const scale = i === 0 ? 1 : (lo + hi) / 2;
|
|
21
|
+
const l = (L + 0.3963377774 * a * scale + 0.2158037573 * b * scale) ** 3;
|
|
22
|
+
const m = (L - 0.1055613458 * a * scale - 0.0638541728 * b * scale) ** 3;
|
|
23
|
+
const s = (L - 0.0894841775 * a * scale - 1.291485548 * b * scale) ** 3;
|
|
24
|
+
const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
25
|
+
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
26
|
+
const blue = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
|
27
|
+
if (Math.min(r, g, blue) >= 0 && Math.max(r, g, blue) <= 1) {
|
|
28
|
+
result = (byte(r) << 16) | (byte(g) << 8) | byte(blue);
|
|
29
|
+
if (i === 0)
|
|
30
|
+
return result;
|
|
31
|
+
lo = scale;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
hi = scale;
|
|
35
|
+
}
|
|
36
|
+
return result < 0 ? byte(L ** 3) * 0x10101 : result;
|
|
37
|
+
}
|
|
38
|
+
/** Leave passing pairs exact. Search both lightness directions with a verified endpoint fallback. */
|
|
39
|
+
export function correctContrastForeground(fg, bg) {
|
|
40
|
+
if (packedContrastRatio(fg, bg) >= 4.5)
|
|
41
|
+
return fg;
|
|
42
|
+
const r = LINEAR[(fg >>> 16) & 255], g = LINEAR[(fg >>> 8) & 255], b = LINEAR[fg & 255];
|
|
43
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
44
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
45
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
46
|
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
47
|
+
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
48
|
+
const bb = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
49
|
+
let best = 0, distance = Infinity;
|
|
50
|
+
for (const end of [0, 1]) {
|
|
51
|
+
let passing = end, failing = L;
|
|
52
|
+
let rgb = end === 0 ? 0 : 0xffffff;
|
|
53
|
+
if (packedContrastRatio(rgb, bg) < 4.5)
|
|
54
|
+
continue;
|
|
55
|
+
for (let i = 0; i < 12; i++) {
|
|
56
|
+
const mid = (passing + failing) / 2;
|
|
57
|
+
const trial = candidate(mid, a, bb);
|
|
58
|
+
if (packedContrastRatio(trial, bg) >= 4.5) {
|
|
59
|
+
passing = mid;
|
|
60
|
+
rgb = trial;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
failing = mid;
|
|
64
|
+
}
|
|
65
|
+
if (Math.abs(passing - L) < distance) {
|
|
66
|
+
best = rgb;
|
|
67
|
+
distance = Math.abs(passing - L);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
/** Cheap high-color-churn fallback: linear-sRGB blend to the nearer passing endpoint. */
|
|
73
|
+
export function fastContrastForeground(fg, bg) {
|
|
74
|
+
if (packedContrastRatio(fg, bg) >= 4.5)
|
|
75
|
+
return fg;
|
|
76
|
+
const y = luminance(fg), base = luminance(bg);
|
|
77
|
+
const darkTarget = (base + 0.05) / 4.5 - 0.05;
|
|
78
|
+
const lightTarget = (base + 0.05) * 4.5 - 0.05;
|
|
79
|
+
const darkMix = darkTarget >= 0 ? (y - darkTarget) / y : Infinity;
|
|
80
|
+
const lightMix = lightTarget <= 1 ? (lightTarget - y) / (1 - y) : Infinity;
|
|
81
|
+
const end = darkMix <= lightMix ? 0 : 1;
|
|
82
|
+
// A small quantization margin usually avoids endpoint fallback. Always verify.
|
|
83
|
+
const amount = Math.min(1, Math.max(0, Math.min(darkMix, lightMix)) + 0.012);
|
|
84
|
+
const r = LINEAR[(fg >>> 16) & 255], g = LINEAR[(fg >>> 8) & 255], b = LINEAR[fg & 255];
|
|
85
|
+
const result = (byte(r + (end - r) * amount) << 16) |
|
|
86
|
+
(byte(g + (end - g) * amount) << 8) |
|
|
87
|
+
byte(b + (end - b) * amount);
|
|
88
|
+
return packedContrastRatio(result, bg) >= 4.5 ? result : end === 0 ? 0 : 0xffffff;
|
|
89
|
+
}
|
|
90
|
+
export function createContrastPairCache(limit = 4096) {
|
|
91
|
+
const pairs = new Map();
|
|
92
|
+
let remaining = Infinity;
|
|
93
|
+
return {
|
|
94
|
+
beginFrame() {
|
|
95
|
+
remaining = 128;
|
|
96
|
+
},
|
|
97
|
+
get size() {
|
|
98
|
+
return pairs.size;
|
|
99
|
+
},
|
|
100
|
+
correct(fg, bg) {
|
|
101
|
+
const key = fg * 0x1000000 + bg;
|
|
102
|
+
const cached = pairs.get(key);
|
|
103
|
+
if (cached !== undefined)
|
|
104
|
+
return cached;
|
|
105
|
+
const corrected = packedContrastRatio(fg, bg) >= 4.5
|
|
106
|
+
? fg
|
|
107
|
+
: remaining-- > 0
|
|
108
|
+
? correctContrastForeground(fg, bg)
|
|
109
|
+
: fastContrastForeground(fg, bg);
|
|
110
|
+
if (pairs.size >= limit)
|
|
111
|
+
pairs.clear();
|
|
112
|
+
pairs.set(key, corrected);
|
|
113
|
+
return corrected;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const read = (c, i) => ((c[i] & 255) << 16) | ((c[i + 1] & 255) << 8) | (c[i + 2] & 255);
|
|
118
|
+
/** Final composed surface only: no canonical state or scheduling; allocation-free cache hits. */
|
|
119
|
+
export function createAutomaticContrastPass() {
|
|
120
|
+
const cache = createContrastPairCache();
|
|
121
|
+
return (buffer) => {
|
|
122
|
+
cache.beginFrame();
|
|
123
|
+
const { char, fg, bg, attributes } = buffer.buffers;
|
|
124
|
+
for (let cell = 0; cell < char.length; cell++) {
|
|
125
|
+
const cp = char[cell], attr = attributes[cell], i = cell * 4;
|
|
126
|
+
// Preserve concealment, whitespace and pixel-art coverage. Grapheme IDs and
|
|
127
|
+
// wide continuations are opaque and retained byte-for-byte.
|
|
128
|
+
if (attr & 64 ||
|
|
129
|
+
cp === 0 ||
|
|
130
|
+
cp === 32 ||
|
|
131
|
+
cp === 160 ||
|
|
132
|
+
(cp >= 0x2580 && cp <= 0x259f) ||
|
|
133
|
+
(cp >= 0x2800 && cp <= 0x28ff))
|
|
134
|
+
continue;
|
|
135
|
+
if ((fg[i + 3] & 255) !== 255 || (bg[i + 3] & 255) !== 255)
|
|
136
|
+
continue;
|
|
137
|
+
const foreground = attr & 32 ? bg : fg;
|
|
138
|
+
const background = attr & 32 ? fg : bg;
|
|
139
|
+
const original = read(foreground, i);
|
|
140
|
+
const base = read(background, i);
|
|
141
|
+
// DIM is emulator-dependent. Materialize a deterministic 50% sRGB blend
|
|
142
|
+
// before correction and clear only the final composed DIM bit.
|
|
143
|
+
let visible = original;
|
|
144
|
+
if (attr & 2) {
|
|
145
|
+
visible =
|
|
146
|
+
(Math.round(((original >>> 16) + (base >>> 16)) / 2) << 16) |
|
|
147
|
+
(Math.round((((original >>> 8) & 255) + ((base >>> 8) & 255)) / 2) << 8) |
|
|
148
|
+
Math.round(((original & 255) + (base & 255)) / 2);
|
|
149
|
+
attributes[cell] = attr & ~2;
|
|
150
|
+
}
|
|
151
|
+
const corrected = cache.correct(visible, base);
|
|
152
|
+
if (corrected === original && !(attr & 2))
|
|
153
|
+
continue;
|
|
154
|
+
// RGB intent is zero. Indexed/default metadata must not override correction.
|
|
155
|
+
foreground[i] = corrected >>> 16;
|
|
156
|
+
foreground[i + 1] = (corrected >>> 8) & 255;
|
|
157
|
+
foreground[i + 2] = corrected & 255;
|
|
158
|
+
foreground[i + 3] = 255;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -1124,7 +1124,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1124
1124
|
}
|
|
1125
1125
|
}) ?? null;
|
|
1126
1126
|
let pendingInitialFit = null;
|
|
1127
|
-
const fitViewport = (cols, rows) => {
|
|
1127
|
+
const fitViewport = (cols, rows, semanticWindowId) => {
|
|
1128
1128
|
if (closed)
|
|
1129
1129
|
return Promise.resolve("geometry-authority-conflict");
|
|
1130
1130
|
// Keep only the latest requested geometry while hidden seeds arrive. All
|
|
@@ -1133,23 +1133,29 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1133
1133
|
if (pendingInitialFit) {
|
|
1134
1134
|
pendingInitialFit.cols = cols;
|
|
1135
1135
|
pendingInitialFit.rows = rows;
|
|
1136
|
+
pendingInitialFit.semanticWindowId = semanticWindowId;
|
|
1136
1137
|
return pendingInitialFit.result;
|
|
1137
1138
|
}
|
|
1138
1139
|
const request = {
|
|
1139
1140
|
cols,
|
|
1140
1141
|
rows,
|
|
1142
|
+
semanticWindowId,
|
|
1141
1143
|
result: Promise.resolve("geometry-authority-conflict"),
|
|
1142
1144
|
};
|
|
1143
1145
|
pendingInitialFit = request;
|
|
1144
1146
|
request.result = allSeeds.then((ready) => {
|
|
1145
1147
|
pendingInitialFit = null;
|
|
1146
|
-
return ready
|
|
1148
|
+
return ready
|
|
1149
|
+
? fitViewport(request.cols, request.rows, request.semanticWindowId)
|
|
1150
|
+
: "geometry-authority-conflict";
|
|
1147
1151
|
});
|
|
1148
1152
|
return request.result;
|
|
1149
1153
|
}
|
|
1150
1154
|
if ([...endpoints.values()].some((endpoint) => !endpoint.inputReady))
|
|
1151
1155
|
return Promise.resolve("geometry-authority-conflict");
|
|
1152
|
-
return
|
|
1156
|
+
return (semanticWindowId === undefined
|
|
1157
|
+
? opened.fitViewport(cols, rows)
|
|
1158
|
+
: opened.fitViewport(cols, rows, semanticWindowId)).catch((error) => {
|
|
1153
1159
|
if (error instanceof PaneStreamOperationError && error.code === "authority-rejected")
|
|
1154
1160
|
return "geometry-authority-conflict";
|
|
1155
1161
|
throw error;
|
|
@@ -422,11 +422,11 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
422
422
|
if (v === this._presentationGeneration)
|
|
423
423
|
return;
|
|
424
424
|
this._presentationGeneration = v;
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
this.
|
|
429
|
-
this.
|
|
425
|
+
// Composition reuses the retained framebuffer. Geometry reallocation,
|
|
426
|
+
// source replacement and palette changes independently force a full walk.
|
|
427
|
+
// Reclaim the hardware cursor at the new absolute position on visibility.
|
|
428
|
+
this._needsCursorPresentation = true;
|
|
429
|
+
this.requestRender();
|
|
430
430
|
}
|
|
431
431
|
set sourceEpoch(v) {
|
|
432
432
|
if (v === this._sourceEpoch)
|
|
@@ -453,6 +453,8 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
453
453
|
// Detailed focus correlation never owns renderer replacement.
|
|
454
454
|
}
|
|
455
455
|
this._rendererEpoch = v;
|
|
456
|
+
this._forceFull = true;
|
|
457
|
+
this.invalidate();
|
|
456
458
|
}
|
|
457
459
|
set hostFocusTransitionOwner(v) {
|
|
458
460
|
if (v !== this._hostFocusTransitionOwner)
|