pi-tmux-session-map 0.3.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/LICENSE +21 -0
- package/README.md +123 -0
- package/package.json +56 -0
- package/scripts/benchmark-overhead.mjs +165 -0
- package/scripts/check-installed-cache.mjs +53 -0
- package/src/domain/async-queue.ts +18 -0
- package/src/domain/atomic-write.ts +193 -0
- package/src/domain/error-reporter.ts +39 -0
- package/src/domain/pane-info.ts +92 -0
- package/src/domain/pane-key.ts +60 -0
- package/src/domain/restore-ack.ts +77 -0
- package/src/domain/session-mapping.ts +89 -0
- package/src/domain/state.ts +118 -0
- package/src/domain/status.ts +65 -0
- package/src/index.ts +405 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { normalizePaneKey } from "./pane-key.ts";
|
|
2
|
+
import type { PaneInfo } from "./status.ts";
|
|
3
|
+
|
|
4
|
+
export function parseTmuxIndex(value: string): number {
|
|
5
|
+
const parsed = Number.parseInt(value, 10);
|
|
6
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parsePaneInfoOutput(stdout: string, paneId: string): PaneInfo | undefined {
|
|
10
|
+
const trimmed = stdout.trim();
|
|
11
|
+
if (trimmed === "") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [rawPaneKey, sessionName, rawWindowIndex, rawPaneIndex] = trimmed.split("\t");
|
|
16
|
+
const paneKey = normalizePaneKey(rawPaneKey ?? "");
|
|
17
|
+
if (!paneKey || !sessionName) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
backend: "tmux",
|
|
23
|
+
paneKey,
|
|
24
|
+
paneId,
|
|
25
|
+
sessionName,
|
|
26
|
+
windowIndex: parseTmuxIndex(rawWindowIndex ?? "0"),
|
|
27
|
+
paneIndex: parseTmuxIndex(rawPaneIndex ?? "0"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ZellijPane = {
|
|
32
|
+
id?: unknown;
|
|
33
|
+
is_plugin?: unknown;
|
|
34
|
+
tab_position?: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function isZellijPane(value: unknown): value is ZellijPane {
|
|
38
|
+
return typeof value === "object" && value !== null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mexPaneInfo(sessionId: string | undefined): PaneInfo | undefined {
|
|
42
|
+
if (!sessionId) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
backend: "mex",
|
|
47
|
+
paneKey: `mex:${sessionId}`,
|
|
48
|
+
paneId: sessionId,
|
|
49
|
+
sessionName: sessionId,
|
|
50
|
+
windowIndex: 0,
|
|
51
|
+
paneIndex: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseZellijPaneInfo(
|
|
56
|
+
stdout: string,
|
|
57
|
+
sessionName: string,
|
|
58
|
+
paneId: string,
|
|
59
|
+
): PaneInfo | undefined {
|
|
60
|
+
if (!sessionName || !paneId) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
let raw: unknown;
|
|
64
|
+
try {
|
|
65
|
+
raw = JSON.parse(stdout);
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(raw)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const numericId = Number.parseInt(paneId, 10);
|
|
73
|
+
if (!Number.isFinite(numericId)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const pane = raw.find(
|
|
77
|
+
(entry) => isZellijPane(entry) && entry.is_plugin !== true && entry.id === numericId,
|
|
78
|
+
);
|
|
79
|
+
if (!isZellijPane(pane)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const tabPosition = typeof pane.tab_position === "number" ? pane.tab_position : 0;
|
|
83
|
+
const normalizedPaneId = `terminal_${numericId}`;
|
|
84
|
+
return {
|
|
85
|
+
backend: "zellij",
|
|
86
|
+
paneKey: `${sessionName}:${tabPosition}.${numericId}`,
|
|
87
|
+
paneId: normalizedPaneId,
|
|
88
|
+
sessionName,
|
|
89
|
+
windowIndex: tabPosition,
|
|
90
|
+
paneIndex: numericId,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** tmux format string that yields a pane key stable across tmux server restarts. */
|
|
6
|
+
export const PANE_KEY_FORMAT = "#{session_name}:#{window_index}.#{pane_index}";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default state directory for pane -> session-file mappings.
|
|
10
|
+
*
|
|
11
|
+
* One plain-text file per pane; the first line is the absolute path of the
|
|
12
|
+
* Pi session file that ran in that pane.
|
|
13
|
+
*/
|
|
14
|
+
export function defaultStateDir(): string {
|
|
15
|
+
return join(homedir(), ".local", "state", "pi", "tmux-sessions");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize a pane key for use as a file name.
|
|
20
|
+
*
|
|
21
|
+
* Keep in sync with consumers that resolve the mapping from shell, e.g. a
|
|
22
|
+
* `pi-tmux-resume` wrapper using `tr -c 'A-Za-z0-9._-' '_'`.
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeKey(key: string): string {
|
|
25
|
+
let result = "";
|
|
26
|
+
for (const byte of Buffer.from(key, "utf8")) {
|
|
27
|
+
const isDigit = byte >= 0x30 && byte <= 0x39;
|
|
28
|
+
const isUpper = byte >= 0x41 && byte <= 0x5a;
|
|
29
|
+
const isLower = byte >= 0x61 && byte <= 0x7a;
|
|
30
|
+
const isPunctuation = byte === 0x2e || byte === 0x5f || byte === 0x2d;
|
|
31
|
+
result += isDigit || isUpper || isLower || isPunctuation ? String.fromCharCode(byte) : "_";
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hashKey(key: string): string {
|
|
37
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 12);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function keyFileStem(key: string): string {
|
|
41
|
+
const sanitized = sanitizeKey(key).slice(0, 120).replace(/^\.+$/, "_");
|
|
42
|
+
const prefix = sanitized === "" ? "pane" : sanitized;
|
|
43
|
+
return `${prefix}-${hashKey(key)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** File name of the mapping file for a raw pane key. */
|
|
47
|
+
export function mappingFileName(key: string): string {
|
|
48
|
+
return `${keyFileStem(key)}.session`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** File name of the tws status file for a raw pane key. */
|
|
52
|
+
export function statusFileName(key: string): string {
|
|
53
|
+
return `${keyFileStem(key)}.json`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Normalize raw `tmux display-message` output into a pane key, if usable. */
|
|
57
|
+
export function normalizePaneKey(stdout: string): string | undefined {
|
|
58
|
+
const key = stdout.trim();
|
|
59
|
+
return key === "" ? undefined : key;
|
|
60
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { PaneInfo, SessionInfo } from "./status.ts";
|
|
4
|
+
|
|
5
|
+
export type RestoreExpectation = {
|
|
6
|
+
token: string;
|
|
7
|
+
paneKey: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
sessionFile: string;
|
|
10
|
+
ackDir: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RestoreAck = {
|
|
14
|
+
schema: 1;
|
|
15
|
+
status: "resumed" | "mismatch";
|
|
16
|
+
token: string;
|
|
17
|
+
expectedPaneKey: string;
|
|
18
|
+
actualPaneKey: string;
|
|
19
|
+
expectedSessionId: string;
|
|
20
|
+
actualSessionId: string;
|
|
21
|
+
expectedSessionFile: string;
|
|
22
|
+
actualSessionFile: string | null;
|
|
23
|
+
cwd: string;
|
|
24
|
+
sessionName: string | null;
|
|
25
|
+
reason: string;
|
|
26
|
+
acknowledgedAtMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function restoreExpectationFromEnvironment(
|
|
30
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
31
|
+
): RestoreExpectation | undefined {
|
|
32
|
+
const token = env.PI_TMUX_RESTORE_TOKEN;
|
|
33
|
+
const paneKey = env.PI_TMUX_EXPECTED_PANE;
|
|
34
|
+
const sessionId = env.PI_TMUX_EXPECTED_SESSION_ID;
|
|
35
|
+
const sessionFile = env.PI_TMUX_EXPECTED_SESSION_FILE;
|
|
36
|
+
const ackDir = env.PI_TMUX_RESTORE_ACK_DIR;
|
|
37
|
+
if (!token || !paneKey || !sessionId || !sessionFile || !ackDir) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return { token, paneKey, sessionId, sessionFile, ackDir };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildRestoreAck(
|
|
44
|
+
expectation: RestoreExpectation,
|
|
45
|
+
pane: PaneInfo,
|
|
46
|
+
session: SessionInfo,
|
|
47
|
+
sessionId: string,
|
|
48
|
+
reason: string,
|
|
49
|
+
acknowledgedAtMs: number,
|
|
50
|
+
): RestoreAck {
|
|
51
|
+
const matches = pane.paneKey === expectation.paneKey
|
|
52
|
+
&& sessionId === expectation.sessionId
|
|
53
|
+
&& session.sessionFile === expectation.sessionFile;
|
|
54
|
+
return {
|
|
55
|
+
schema: 1,
|
|
56
|
+
status: matches ? "resumed" : "mismatch",
|
|
57
|
+
token: expectation.token,
|
|
58
|
+
expectedPaneKey: expectation.paneKey,
|
|
59
|
+
actualPaneKey: pane.paneKey,
|
|
60
|
+
expectedSessionId: expectation.sessionId,
|
|
61
|
+
actualSessionId: sessionId,
|
|
62
|
+
expectedSessionFile: expectation.sessionFile,
|
|
63
|
+
actualSessionFile: session.sessionFile,
|
|
64
|
+
cwd: session.cwd,
|
|
65
|
+
sessionName: session.sessionName,
|
|
66
|
+
reason,
|
|
67
|
+
acknowledgedAtMs,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function restoreAckPath(expectation: RestoreExpectation): string {
|
|
72
|
+
return join(expectation.ackDir, `${expectation.token}.json`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function serializeRestoreAck(ack: RestoreAck): string {
|
|
76
|
+
return `${JSON.stringify(ack)}\n`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type SessionMapping = {
|
|
2
|
+
schema: 2;
|
|
3
|
+
paneKey: string;
|
|
4
|
+
sessionFile: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
sessionName: string | null;
|
|
8
|
+
updatedAtMs: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ParsedSessionMapping =
|
|
12
|
+
| { format: "structured"; mapping: SessionMapping }
|
|
13
|
+
| { format: "legacy"; sessionFile: string };
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildSessionMapping(
|
|
20
|
+
paneKey: string,
|
|
21
|
+
sessionFile: string,
|
|
22
|
+
sessionId: string,
|
|
23
|
+
cwd: string,
|
|
24
|
+
sessionName: string | null,
|
|
25
|
+
updatedAtMs: number,
|
|
26
|
+
): SessionMapping {
|
|
27
|
+
return {
|
|
28
|
+
schema: 2,
|
|
29
|
+
paneKey,
|
|
30
|
+
sessionFile,
|
|
31
|
+
sessionId,
|
|
32
|
+
cwd,
|
|
33
|
+
sessionName,
|
|
34
|
+
updatedAtMs,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function serializeSessionMapping(mapping: SessionMapping): string {
|
|
39
|
+
return `${JSON.stringify(mapping)}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseSessionMapping(content: string): ParsedSessionMapping | undefined {
|
|
43
|
+
const trimmed = content.trim();
|
|
44
|
+
if (trimmed === "") {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (!trimmed.startsWith("{")) {
|
|
48
|
+
return { format: "legacy", sessionFile: trimmed.split(/\r?\n/, 1)[0] ?? "" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let value: unknown;
|
|
52
|
+
try {
|
|
53
|
+
value = JSON.parse(trimmed);
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!isRecord(value)
|
|
59
|
+
|| value.schema !== 2
|
|
60
|
+
|| typeof value.paneKey !== "string"
|
|
61
|
+
|| typeof value.sessionFile !== "string"
|
|
62
|
+
|| typeof value.sessionId !== "string"
|
|
63
|
+
|| typeof value.cwd !== "string"
|
|
64
|
+
|| !(typeof value.sessionName === "string" || value.sessionName === null)
|
|
65
|
+
|| typeof value.updatedAtMs !== "number"
|
|
66
|
+
) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
format: "structured",
|
|
71
|
+
mapping: {
|
|
72
|
+
schema: 2,
|
|
73
|
+
paneKey: value.paneKey,
|
|
74
|
+
sessionFile: value.sessionFile,
|
|
75
|
+
sessionId: value.sessionId,
|
|
76
|
+
cwd: value.cwd,
|
|
77
|
+
sessionName: value.sessionName,
|
|
78
|
+
updatedAtMs: value.updatedAtMs,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function mappedSessionFile(content: string): string | undefined {
|
|
84
|
+
const parsed = parseSessionMapping(content);
|
|
85
|
+
if (!parsed) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return parsed.format === "structured" ? parsed.mapping.sessionFile : parsed.sessionFile;
|
|
89
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type WorkState =
|
|
2
|
+
| "idle"
|
|
3
|
+
| "working"
|
|
4
|
+
| "retrying"
|
|
5
|
+
| "done"
|
|
6
|
+
| "cancelled"
|
|
7
|
+
| "incomplete"
|
|
8
|
+
| "failed"
|
|
9
|
+
| "shutdown";
|
|
10
|
+
|
|
11
|
+
export type FinalWorkState = "done" | "cancelled" | "incomplete" | "failed";
|
|
12
|
+
|
|
13
|
+
function isFinalWorkState(state: WorkState | null): state is FinalWorkState {
|
|
14
|
+
return state === "done" || state === "cancelled" || state === "incomplete" || state === "failed";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RuntimeState = {
|
|
18
|
+
startedAtMs: number | null;
|
|
19
|
+
finishedAtMs: number | null;
|
|
20
|
+
lastState: WorkState | null;
|
|
21
|
+
sequence: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StatusTransition = RuntimeState & {
|
|
25
|
+
state: WorkState;
|
|
26
|
+
shouldWrite: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function createRuntimeState(): RuntimeState {
|
|
30
|
+
return {
|
|
31
|
+
startedAtMs: null,
|
|
32
|
+
finishedAtMs: null,
|
|
33
|
+
lastState: null,
|
|
34
|
+
sequence: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function nextStatusTransition(
|
|
39
|
+
current: RuntimeState,
|
|
40
|
+
state: WorkState,
|
|
41
|
+
nowMs: number,
|
|
42
|
+
): StatusTransition {
|
|
43
|
+
const sequence = current.sequence + 1;
|
|
44
|
+
|
|
45
|
+
if (state === "shutdown" && isFinalWorkState(current.lastState)) {
|
|
46
|
+
return {
|
|
47
|
+
...current,
|
|
48
|
+
sequence,
|
|
49
|
+
state,
|
|
50
|
+
shouldWrite: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (state === "working") {
|
|
55
|
+
return {
|
|
56
|
+
startedAtMs: nowMs,
|
|
57
|
+
finishedAtMs: null,
|
|
58
|
+
lastState: "working",
|
|
59
|
+
sequence,
|
|
60
|
+
state,
|
|
61
|
+
shouldWrite: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (state === "retrying") {
|
|
66
|
+
return {
|
|
67
|
+
startedAtMs: current.startedAtMs ?? nowMs,
|
|
68
|
+
finishedAtMs: null,
|
|
69
|
+
lastState: "retrying",
|
|
70
|
+
sequence,
|
|
71
|
+
state,
|
|
72
|
+
shouldWrite: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isFinalWorkState(state)) {
|
|
77
|
+
return {
|
|
78
|
+
startedAtMs: current.startedAtMs,
|
|
79
|
+
finishedAtMs: current.finishedAtMs ?? nowMs,
|
|
80
|
+
lastState: state,
|
|
81
|
+
sequence,
|
|
82
|
+
state,
|
|
83
|
+
shouldWrite: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
startedAtMs: state === "idle" ? null : current.startedAtMs,
|
|
89
|
+
finishedAtMs: state === "idle" ? null : current.finishedAtMs,
|
|
90
|
+
lastState: state,
|
|
91
|
+
sequence,
|
|
92
|
+
state,
|
|
93
|
+
shouldWrite: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function refreshStatusTransition(current: RuntimeState, nowMs: number): StatusTransition {
|
|
98
|
+
const state = current.lastState ?? "idle";
|
|
99
|
+
|
|
100
|
+
if (state === "idle" && current.lastState === null) {
|
|
101
|
+
return nextStatusTransition(current, "idle", nowMs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (state === "working" && current.startedAtMs === null) {
|
|
105
|
+
return nextStatusTransition(current, "working", nowMs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isFinalWorkState(state) && current.finishedAtMs === null) {
|
|
109
|
+
return nextStatusTransition(current, state, nowMs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...current,
|
|
114
|
+
sequence: current.sequence + 1,
|
|
115
|
+
state,
|
|
116
|
+
shouldWrite: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { WorkState } from "./state.ts";
|
|
2
|
+
|
|
3
|
+
export type PaneInfo = {
|
|
4
|
+
backend: "tmux" | "zellij" | "mex";
|
|
5
|
+
paneKey: string;
|
|
6
|
+
paneId: string;
|
|
7
|
+
sessionName: string;
|
|
8
|
+
windowIndex: number;
|
|
9
|
+
paneIndex: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SessionInfo = {
|
|
13
|
+
cwd: string;
|
|
14
|
+
sessionFile: string | null;
|
|
15
|
+
sessionName: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TwsStatusPayload = {
|
|
19
|
+
schema: 1;
|
|
20
|
+
agent: "pi";
|
|
21
|
+
backend: "tmux" | "zellij" | "mex";
|
|
22
|
+
pane_id: string;
|
|
23
|
+
pane_key: string;
|
|
24
|
+
tmux_session_name: string;
|
|
25
|
+
window_index: number;
|
|
26
|
+
pane_index: number;
|
|
27
|
+
cwd: string;
|
|
28
|
+
session_file: string | null;
|
|
29
|
+
session_name: string | null;
|
|
30
|
+
state: WorkState;
|
|
31
|
+
updated_at_ms: number;
|
|
32
|
+
started_at_ms: number | null;
|
|
33
|
+
finished_at_ms: number | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function buildTwsStatusPayload(
|
|
37
|
+
pane: PaneInfo,
|
|
38
|
+
session: SessionInfo,
|
|
39
|
+
state: WorkState,
|
|
40
|
+
updatedAtMs: number,
|
|
41
|
+
startedAtMs: number | null,
|
|
42
|
+
finishedAtMs: number | null,
|
|
43
|
+
): TwsStatusPayload {
|
|
44
|
+
return {
|
|
45
|
+
schema: 1,
|
|
46
|
+
agent: "pi",
|
|
47
|
+
backend: pane.backend,
|
|
48
|
+
pane_id: pane.paneId,
|
|
49
|
+
pane_key: pane.paneKey,
|
|
50
|
+
tmux_session_name: pane.sessionName,
|
|
51
|
+
window_index: pane.windowIndex,
|
|
52
|
+
pane_index: pane.paneIndex,
|
|
53
|
+
cwd: session.cwd,
|
|
54
|
+
session_file: session.sessionFile,
|
|
55
|
+
session_name: session.sessionName,
|
|
56
|
+
state,
|
|
57
|
+
updated_at_ms: updatedAtMs,
|
|
58
|
+
started_at_ms: startedAtMs,
|
|
59
|
+
finished_at_ms: finishedAtMs,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function serializeStatusPayload(payload: TwsStatusPayload): string {
|
|
64
|
+
return `${JSON.stringify(payload)}\n`;
|
|
65
|
+
}
|