@tryinget/pi-activity-strip 0.4.0 → 0.6.0
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 +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "binds a running Codex process to its session thread and rollout file"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing Codex session identification or its thread-index reader"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Codex keeps an index of its sessions in a versioned SQLite database under its home, one row per
|
|
15
|
+
* thread with the rollout path, working directory and title. It only opens the rollout once a task
|
|
16
|
+
* runs, so the index is what links a live process to its session before any work starts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const CODEX_STATE_DB = /^state_(\d+)\.sqlite$/;
|
|
20
|
+
export const CODEX_THREAD_LIMIT = 400;
|
|
21
|
+
/** A thread is created moments after its process starts; allow for clock and rounding slack. */
|
|
22
|
+
export const CODEX_THREAD_START_SLACK_MS = 5000;
|
|
23
|
+
|
|
24
|
+
/** @typedef {{id: string; rolloutPath: string; cwd: string; title: string; createdAtMs: number; updatedAtMs: number}} CodexThread */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Newest thread-index database in a Codex home. The suffix is a schema version, so the highest one
|
|
28
|
+
* is current; a home with none yields "".
|
|
29
|
+
* @param {string} codexHome
|
|
30
|
+
* @param {Pick<typeof fs, "readdirSync">} [fsImpl]
|
|
31
|
+
*/
|
|
32
|
+
export function resolveCodexStateDb(codexHome, fsImpl = fs) {
|
|
33
|
+
let best = { version: -1, file: "" };
|
|
34
|
+
let entries = [];
|
|
35
|
+
try {
|
|
36
|
+
entries = fsImpl.readdirSync(codexHome);
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const match = CODEX_STATE_DB.exec(entry);
|
|
42
|
+
if (!match) continue;
|
|
43
|
+
const version = Number.parseInt(match[1] ?? "", 10);
|
|
44
|
+
if (Number.isInteger(version) && version > best.version) best = { version, file: entry };
|
|
45
|
+
}
|
|
46
|
+
return best.file ? path.join(codexHome, best.file) : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the thread index. The database belongs to a running application, so it is opened read-only
|
|
51
|
+
* and every failure — a missing file, a locked database, or a runtime without `node:sqlite` —
|
|
52
|
+
* yields an empty index rather than an error.
|
|
53
|
+
* @param {string} databasePath
|
|
54
|
+
* @returns {CodexThread[]}
|
|
55
|
+
*/
|
|
56
|
+
export function readCodexThreads(databasePath) {
|
|
57
|
+
if (!databasePath) return [];
|
|
58
|
+
try {
|
|
59
|
+
// Loaded here so a runtime without the built-in SQLite module simply reports no threads.
|
|
60
|
+
const { DatabaseSync } = requireBuiltin("node:sqlite");
|
|
61
|
+
const db = new DatabaseSync(databasePath, { readOnly: true });
|
|
62
|
+
try {
|
|
63
|
+
const rows = db
|
|
64
|
+
.prepare(
|
|
65
|
+
"SELECT id, rollout_path, cwd, title, created_at_ms, updated_at_ms FROM threads WHERE archived = 0 ORDER BY created_at_ms DESC LIMIT ?",
|
|
66
|
+
)
|
|
67
|
+
.all(CODEX_THREAD_LIMIT);
|
|
68
|
+
return rows.map(
|
|
69
|
+
/** @param {any} row */ (row) => ({
|
|
70
|
+
id: String(row.id ?? ""),
|
|
71
|
+
rolloutPath: String(row.rollout_path ?? ""),
|
|
72
|
+
cwd: String(row.cwd ?? ""),
|
|
73
|
+
title: String(row.title ?? ""),
|
|
74
|
+
createdAtMs: Number(row.created_at_ms ?? 0),
|
|
75
|
+
updatedAtMs: Number(row.updated_at_ms ?? 0),
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
} finally {
|
|
79
|
+
db.close();
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Bind one Codex process to its thread. An open rollout descriptor is exact proof and wins. With
|
|
88
|
+
* no descriptor, a thread qualifies only when it was created after the process started and in the
|
|
89
|
+
* same directory; anything ambiguous binds nothing rather than guessing.
|
|
90
|
+
* @param {{processId: number; cwd: string; startedAt: number; openPaths?: string[]}} process
|
|
91
|
+
* @param {CodexThread[]} threads
|
|
92
|
+
* @returns {CodexThread | null}
|
|
93
|
+
*/
|
|
94
|
+
export function bindCodexThread({ cwd, startedAt, openPaths = [] }, threads) {
|
|
95
|
+
const open = new Set(openPaths.filter(Boolean));
|
|
96
|
+
if (open.size > 0) {
|
|
97
|
+
const byRollout = threads.filter(
|
|
98
|
+
(thread) => thread.rolloutPath && open.has(thread.rolloutPath),
|
|
99
|
+
);
|
|
100
|
+
if (byRollout.length === 1) return byRollout[0];
|
|
101
|
+
if (byRollout.length > 1) return null;
|
|
102
|
+
}
|
|
103
|
+
const normalizedCwd = String(cwd ?? "").trim();
|
|
104
|
+
if (!normalizedCwd || !Number.isFinite(startedAt) || startedAt <= 0) return null;
|
|
105
|
+
const candidates = threads.filter(
|
|
106
|
+
(thread) =>
|
|
107
|
+
thread.cwd === normalizedCwd &&
|
|
108
|
+
thread.createdAtMs > 0 &&
|
|
109
|
+
thread.createdAtMs >= startedAt - CODEX_THREAD_START_SLACK_MS,
|
|
110
|
+
);
|
|
111
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rollout files a process currently holds open, which is exact evidence of the session it is
|
|
116
|
+
* running.
|
|
117
|
+
* @param {number} processId
|
|
118
|
+
* @param {{readDir: (path: string) => string[]; readLink: (path: string) => string}} procfs
|
|
119
|
+
*/
|
|
120
|
+
export function openRolloutPaths(processId, procfs) {
|
|
121
|
+
const fdDir = path.join("/proc", String(processId), "fd");
|
|
122
|
+
const paths = [];
|
|
123
|
+
for (const entry of procfs.readDir(fdDir)) {
|
|
124
|
+
const target = procfs.readLink(path.join(fdDir, entry));
|
|
125
|
+
if (target.includes("/sessions/") && target.endsWith(".jsonl")) paths.push(target);
|
|
126
|
+
}
|
|
127
|
+
return paths;
|
|
128
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ---
|
|
3
|
+
# summary: "enumerates Ghostty windows and their tab labels through AT-SPI for hidden-tab placement"
|
|
4
|
+
# read_when:
|
|
5
|
+
# - "changing how the activity strip discovers tabs hidden behind another Ghostty tab"
|
|
6
|
+
# ---
|
|
7
|
+
"""Print one JSON document describing every AT-SPI frame of the given process ids.
|
|
8
|
+
|
|
9
|
+
Usage: ghostty-tab-inventory.py <pid> [<pid> ...]
|
|
10
|
+
|
|
11
|
+
Output: {"ok": true, "frames": [{"pid": int, "name": str, "tabs": [str, ...]}, ...]}
|
|
12
|
+
Exit 3 with {"ok": false, "error": ...} when the AT-SPI Python bindings are unavailable.
|
|
13
|
+
The script only reads accessibility state; it never activates, selects, or focuses anything.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
MAX_NODES_PER_FRAME = 20000
|
|
20
|
+
MAX_DEPTH = 40
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
try:
|
|
25
|
+
import gi
|
|
26
|
+
|
|
27
|
+
gi.require_version("Atspi", "2.0")
|
|
28
|
+
from gi.repository import Atspi
|
|
29
|
+
except Exception as error: # noqa: BLE001 - any import failure means the capability is absent
|
|
30
|
+
print(json.dumps({"ok": False, "error": f"AT-SPI bindings unavailable: {error}"}))
|
|
31
|
+
return 3
|
|
32
|
+
|
|
33
|
+
pids = {int(argument) for argument in sys.argv[1:] if argument.isdigit()}
|
|
34
|
+
if not pids:
|
|
35
|
+
print(json.dumps({"ok": True, "frames": []}))
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
def tab_labels(frame):
|
|
39
|
+
labels = []
|
|
40
|
+
stack = [(frame, 0)]
|
|
41
|
+
seen = 0
|
|
42
|
+
while stack and seen < MAX_NODES_PER_FRAME:
|
|
43
|
+
node, depth = stack.pop()
|
|
44
|
+
seen += 1
|
|
45
|
+
try:
|
|
46
|
+
role = node.get_role()
|
|
47
|
+
except Exception: # noqa: BLE001 - a vanished node is skipped
|
|
48
|
+
continue
|
|
49
|
+
if role == Atspi.Role.PAGE_TAB:
|
|
50
|
+
try:
|
|
51
|
+
labels.append(node.get_name() or "")
|
|
52
|
+
except Exception: # noqa: BLE001
|
|
53
|
+
pass
|
|
54
|
+
continue
|
|
55
|
+
if depth >= MAX_DEPTH:
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
count = node.get_child_count()
|
|
59
|
+
except Exception: # noqa: BLE001
|
|
60
|
+
continue
|
|
61
|
+
for index in range(count):
|
|
62
|
+
try:
|
|
63
|
+
child = node.get_child_at_index(index)
|
|
64
|
+
except Exception: # noqa: BLE001
|
|
65
|
+
continue
|
|
66
|
+
if child is not None:
|
|
67
|
+
stack.append((child, depth + 1))
|
|
68
|
+
return labels
|
|
69
|
+
|
|
70
|
+
frames = []
|
|
71
|
+
desktop = Atspi.get_desktop(0)
|
|
72
|
+
for index in range(desktop.get_child_count()):
|
|
73
|
+
application = desktop.get_child_at_index(index)
|
|
74
|
+
if application is None:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
pid = application.get_process_id()
|
|
78
|
+
except Exception: # noqa: BLE001
|
|
79
|
+
continue
|
|
80
|
+
if pid not in pids:
|
|
81
|
+
continue
|
|
82
|
+
for window_index in range(application.get_child_count()):
|
|
83
|
+
frame = application.get_child_at_index(window_index)
|
|
84
|
+
if frame is None:
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
if frame.get_role() != Atspi.Role.FRAME:
|
|
88
|
+
continue
|
|
89
|
+
name = frame.get_name() or ""
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
continue
|
|
92
|
+
frames.append({"pid": pid, "name": name, "tabs": tab_labels(frame)})
|
|
93
|
+
|
|
94
|
+
print(json.dumps({"ok": True, "frames": frames}))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
sys.exit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "restores tiled windows stranded at the wrong height when the ribbon's exclusive zone appears or disappears"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing height repair, its evidence rules, or its safety bounds"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
export const HEIGHT_REPAIR_SETTLE_MS = 300;
|
|
8
|
+
export const HEIGHT_REPAIR_MAX_PER_TRANSITION = 48;
|
|
9
|
+
|
|
10
|
+
/** @typedef {{id?: unknown; is_floating?: unknown; layout?: {window_size?: number[]}}} NiriWindow */
|
|
11
|
+
/** @typedef {{id: number; height: number}} StaleWindow */
|
|
12
|
+
|
|
13
|
+
/** @param {NiriWindow} window */
|
|
14
|
+
function tiledHeight(window) {
|
|
15
|
+
if (window?.is_floating === true) return 0;
|
|
16
|
+
const height = Number(window?.layout?.window_size?.[1] ?? 0);
|
|
17
|
+
return Number.isFinite(height) && height > 0 ? height : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @param {NiriWindow[]} windows */
|
|
21
|
+
function heightsById(windows) {
|
|
22
|
+
/** @type {Map<number, number>} */
|
|
23
|
+
const heights = new Map();
|
|
24
|
+
for (const window of windows ?? []) {
|
|
25
|
+
if (!Number.isInteger(window?.id)) continue;
|
|
26
|
+
const height = tiledHeight(window);
|
|
27
|
+
if (height > 0) heights.set(Number(window.id), height);
|
|
28
|
+
}
|
|
29
|
+
return heights;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide which windows the compositor left behind. Changing the exclusive zone changes the output
|
|
34
|
+
* working area, so every window whose height is automatic moves. A window that did not move while
|
|
35
|
+
* others did is only treated as stale when it still sits at a height those movers just vacated:
|
|
36
|
+
* that is positive evidence of the old working area, so a deliberately chosen height of any other
|
|
37
|
+
* value is never touched.
|
|
38
|
+
* @param {NiriWindow[]} before
|
|
39
|
+
* @param {NiriWindow[]} after
|
|
40
|
+
* @returns {StaleWindow[]}
|
|
41
|
+
*/
|
|
42
|
+
export function findStaleWindows(before, after) {
|
|
43
|
+
const previous = heightsById(before);
|
|
44
|
+
const current = heightsById(after);
|
|
45
|
+
/** @type {Set<number>} */
|
|
46
|
+
const vacated = new Set();
|
|
47
|
+
for (const [id, height] of current) {
|
|
48
|
+
const wasHeight = previous.get(id);
|
|
49
|
+
if (wasHeight !== undefined && wasHeight !== height) vacated.add(wasHeight);
|
|
50
|
+
}
|
|
51
|
+
if (vacated.size === 0) return [];
|
|
52
|
+
|
|
53
|
+
/** @type {StaleWindow[]} */
|
|
54
|
+
const stale = [];
|
|
55
|
+
for (const [id, height] of current) {
|
|
56
|
+
const wasHeight = previous.get(id);
|
|
57
|
+
if (wasHeight === undefined || wasHeight !== height) continue;
|
|
58
|
+
if (vacated.has(height)) stale.push({ id, height });
|
|
59
|
+
}
|
|
60
|
+
return stale.sort((left, right) => left.id - right.id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {{
|
|
65
|
+
* execFileAsync: (file: string, args: string[], options: object) => Promise<{stdout?: string}>;
|
|
66
|
+
* env?: NodeJS.ProcessEnv;
|
|
67
|
+
* runtimeStatus: import("../common/contracts.ts").ActivityStripRuntimeStatus;
|
|
68
|
+
* readWindows: () => Promise<NiriWindow[]>;
|
|
69
|
+
* sleep?: (milliseconds: number) => Promise<void>;
|
|
70
|
+
* settleMs?: number;
|
|
71
|
+
* }} options
|
|
72
|
+
*/
|
|
73
|
+
export function createHeightRepair({
|
|
74
|
+
execFileAsync,
|
|
75
|
+
env = process.env,
|
|
76
|
+
runtimeStatus,
|
|
77
|
+
readWindows,
|
|
78
|
+
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
79
|
+
settleMs = HEIGHT_REPAIR_SETTLE_MS,
|
|
80
|
+
}) {
|
|
81
|
+
const enabled = env.PI_ACTIVITY_STRIP_HEIGHT_REPAIR !== "0";
|
|
82
|
+
/** @type {NiriWindow[]} */
|
|
83
|
+
let lastWindows = [];
|
|
84
|
+
/** Windows already reset at a given height, so one pass can never loop on one window. */
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
const repaired = new Set();
|
|
87
|
+
let inFlight = false;
|
|
88
|
+
runtimeStatus.heightRepairState = enabled ? "enabled" : "disabled";
|
|
89
|
+
runtimeStatus.heightRepairCount = 0;
|
|
90
|
+
runtimeStatus.lastHeightRepairAt = 0;
|
|
91
|
+
|
|
92
|
+
/** @param {NiriWindow[]} windows */
|
|
93
|
+
function observe(windows) {
|
|
94
|
+
if (!Array.isArray(windows) || windows.length === 0) return;
|
|
95
|
+
// A window seen at a height other than the one it was reset at has moved since, so a future
|
|
96
|
+
// stranding there is a new event and must not be suppressed by the earlier repair.
|
|
97
|
+
for (const window of windows) {
|
|
98
|
+
if (!Number.isInteger(window?.id)) continue;
|
|
99
|
+
const height = tiledHeight(window);
|
|
100
|
+
if (height <= 0) continue;
|
|
101
|
+
for (const key of repaired) {
|
|
102
|
+
const [id, at] = key.split(":");
|
|
103
|
+
if (Number(id) === Number(window.id) && Number(at) !== height) repaired.delete(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lastWindows = windows;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run after the panel reports that it applied a visibility change, which is the only moment the
|
|
111
|
+
* exclusive zone changes. One pass at a time; a pass never retries a window it already reset.
|
|
112
|
+
*/
|
|
113
|
+
async function repairAfterZoneChange() {
|
|
114
|
+
if (!enabled || inFlight) return [];
|
|
115
|
+
const before = lastWindows;
|
|
116
|
+
if (before.length === 0) return [];
|
|
117
|
+
inFlight = true;
|
|
118
|
+
try {
|
|
119
|
+
await sleep(settleMs);
|
|
120
|
+
const after = await readWindows();
|
|
121
|
+
if (after.length === 0) return [];
|
|
122
|
+
lastWindows = after;
|
|
123
|
+
const stale = findStaleWindows(before, after).filter(
|
|
124
|
+
(window) => !repaired.has(`${window.id}:${window.height}`),
|
|
125
|
+
);
|
|
126
|
+
const bounded = stale.slice(0, HEIGHT_REPAIR_MAX_PER_TRANSITION);
|
|
127
|
+
/** @type {number[]} */
|
|
128
|
+
const applied = [];
|
|
129
|
+
for (const window of bounded) {
|
|
130
|
+
try {
|
|
131
|
+
await execFileAsync(
|
|
132
|
+
"niri",
|
|
133
|
+
["msg", "action", "reset-window-height", "--id", String(window.id)],
|
|
134
|
+
{ env },
|
|
135
|
+
);
|
|
136
|
+
repaired.add(`${window.id}:${window.height}`);
|
|
137
|
+
applied.push(window.id);
|
|
138
|
+
} catch {
|
|
139
|
+
// A window that closed mid-pass, or a rejected action, is left to the next transition.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (applied.length > 0) {
|
|
143
|
+
runtimeStatus.heightRepairCount =
|
|
144
|
+
Number(runtimeStatus.heightRepairCount ?? 0) + applied.length;
|
|
145
|
+
runtimeStatus.lastHeightRepairAt = Date.now();
|
|
146
|
+
}
|
|
147
|
+
return applied;
|
|
148
|
+
} finally {
|
|
149
|
+
inFlight = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { observe, repairAfterZoneChange, enabled };
|
|
154
|
+
}
|