@someonegg/codex-hub 0.0.0-stage → 0.1.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 +101 -2
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/index-Dx1LBBmn.js +304 -0
- package/dist/client/assets/index-lmwuA3r2.css +1 -0
- package/dist/client/index.html +16 -0
- package/dist/server/server/adapters/codex/agent-identity.js +9 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +300 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +107 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +90 -0
- package/dist/server/server/adapters/codex/internal-event-parser.js +56 -0
- package/dist/server/server/adapters/codex/io-errors.js +5 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +58 -0
- package/dist/server/server/adapters/codex/limits.js +10 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/path-policy.js +88 -0
- package/dist/server/server/adapters/codex/project-directory-resolver.js +79 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +100 -0
- package/dist/server/server/adapters/codex/rollout-decoder.js +286 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +62 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +257 -0
- package/dist/server/server/adapters/codex/tool-normalizer.js +58 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +123 -0
- package/dist/server/server/api/session-api-mapper.js +194 -0
- package/dist/server/server/application/session-read-service.js +109 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +103 -0
- package/dist/server/server/config.js +145 -0
- package/dist/server/server/create-hub.js +64 -0
- package/dist/server/server/domain/project-directory.js +5 -0
- package/dist/server/server/domain/session-domain.js +2 -0
- package/dist/server/server/domain/session-text.js +45 -0
- package/dist/server/server/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +63 -0
- package/dist/server/server/http/create-server.js +148 -0
- package/dist/server/server/http/query-params.js +19 -0
- package/dist/server/server/http/request-body.js +48 -0
- package/dist/server/server/http/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +101 -0
- package/dist/server/server/http/session-management-routes.js +90 -0
- package/dist/server/server/http/session-path.js +13 -0
- package/dist/server/server/http/session-read-routes.js +198 -0
- package/dist/server/server/http/session-updates-routes.js +84 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +74 -0
- package/dist/server/server/interaction/interaction-service.js +247 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +60 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +194 -0
- package/dist/server/server/interaction/tmux-launcher.js +75 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +101 -0
- package/dist/server/server/interaction/tmux-service.js +269 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +41 -0
- package/dist/server/server/management/operation-service.js +540 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-manager.js +59 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +304 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/opaque-cursor.js +79 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +151 -0
- package/dist/server/server/repository/session-query-criteria.js +2 -0
- package/dist/server/server/repository/timeline-prefix-index.js +342 -0
- package/dist/server/server/repository/timeline-prefix-registry.js +55 -0
- package/dist/server/server/security/opaque-id.js +13 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +147 -0
- package/dist/server/server/updates/update-revision.js +25 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +12 -0
- package/dist/server/shared/uploads.js +2 -0
- package/package.json +79 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { resolveResumeDirectory, } from "../management/session-manager.js";
|
|
2
|
+
import { paneBindings } from "./pane-binding-manager.js";
|
|
3
|
+
/** Shared, bounded observations. Errors never prove that a pane exited. */
|
|
4
|
+
export class SessionRuntime {
|
|
5
|
+
transport;
|
|
6
|
+
managers;
|
|
7
|
+
bindings;
|
|
8
|
+
#pending = new Map();
|
|
9
|
+
#waiting = [];
|
|
10
|
+
#activeSockets = new Map();
|
|
11
|
+
constructor(transport, managers, bindings = paneBindings) {
|
|
12
|
+
this.transport = transport;
|
|
13
|
+
this.managers = managers;
|
|
14
|
+
this.bindings = bindings;
|
|
15
|
+
}
|
|
16
|
+
probe(attempt) {
|
|
17
|
+
if (!attempt.valid)
|
|
18
|
+
return Promise.reject(new Error("Invalid binding"));
|
|
19
|
+
const key = JSON.stringify([attempt.socketPath, attempt.paneId]);
|
|
20
|
+
const existing = this.#pending.get(key);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const query = (async () => {
|
|
24
|
+
// Different panes on one socket must reach the transport together so they
|
|
25
|
+
// can share one list-panes query, even when there are more than eight panes.
|
|
26
|
+
while (!this.#activeSockets.has(attempt.socketPath) && this.#activeSockets.size >= 8)
|
|
27
|
+
await new Promise((resolve) => this.#waiting.push(resolve));
|
|
28
|
+
this.#activeSockets.set(attempt.socketPath, (this.#activeSockets.get(attempt.socketPath) ?? 0) + 1);
|
|
29
|
+
try {
|
|
30
|
+
return await this.transport.resolveTarget(attempt);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
const remaining = this.#activeSockets.get(attempt.socketPath) - 1;
|
|
34
|
+
if (remaining > 0)
|
|
35
|
+
this.#activeSockets.set(attempt.socketPath, remaining);
|
|
36
|
+
else {
|
|
37
|
+
this.#activeSockets.delete(attempt.socketPath);
|
|
38
|
+
this.#waiting.splice(0).forEach((resume) => resume());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
this.#pending.set(key, query);
|
|
43
|
+
void query.then(() => this.#pending.delete(key), () => this.#pending.delete(key));
|
|
44
|
+
return query;
|
|
45
|
+
}
|
|
46
|
+
async evaluate(session, sessions, detail = true) {
|
|
47
|
+
let status = session.archived ? "archived" : "normal";
|
|
48
|
+
const deny = (reason) => ({ status, resume: { allowed: false, reason } });
|
|
49
|
+
if (session.archived)
|
|
50
|
+
return deny("Archived sessions cannot be resumed");
|
|
51
|
+
const attempt = this.bindings.latest(session.id);
|
|
52
|
+
if (!attempt)
|
|
53
|
+
return deny("No pane binding has been recorded");
|
|
54
|
+
if (!attempt.valid)
|
|
55
|
+
return deny("The latest pane binding is invalid");
|
|
56
|
+
const replacement = this.bindings.replacement(session.id);
|
|
57
|
+
try {
|
|
58
|
+
const target = await this.probe(attempt);
|
|
59
|
+
if (attempt !== this.bindings.latest(session.id) ||
|
|
60
|
+
replacement !== this.bindings.replacement(session.id))
|
|
61
|
+
return deny("Pane binding changed during validation");
|
|
62
|
+
if (target && replacement === null)
|
|
63
|
+
status = "live";
|
|
64
|
+
if (!detail)
|
|
65
|
+
return deny("Resume validation required");
|
|
66
|
+
if (status === "live")
|
|
67
|
+
return deny("Session is already live");
|
|
68
|
+
if (!session.sourceId ||
|
|
69
|
+
!/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(session.sourceId))
|
|
70
|
+
return deny("Session has no valid native identity");
|
|
71
|
+
if (sessions.filter((item) => item.origin.sourceInstanceId === session.origin.sourceInstanceId &&
|
|
72
|
+
item.sourceId === session.sourceId).length !== 1)
|
|
73
|
+
return deny("Native session identity is ambiguous");
|
|
74
|
+
await resolveResumeDirectory(this.managers.get(session.origin.sourceInstanceId), session);
|
|
75
|
+
if (attempt !== this.bindings.latest(session.id) ||
|
|
76
|
+
replacement !== this.bindings.replacement(session.id))
|
|
77
|
+
return deny("Pane binding changed during validation");
|
|
78
|
+
return { status, resume: { allowed: true, reason: null } };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
status = "normal";
|
|
82
|
+
return deny("Runtime or working directory validation failed");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=session-runtime.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Bounded Myers search. Trace stores only frontiers, never historical snapshots. */
|
|
2
|
+
export function diffLines(before, after) {
|
|
3
|
+
const a = before.split("\n");
|
|
4
|
+
const b = after.split("\n");
|
|
5
|
+
let work = 0;
|
|
6
|
+
let frontier = new Map([[1, 0]]);
|
|
7
|
+
const trace = [];
|
|
8
|
+
for (let d = 0; d <= 256; d++) {
|
|
9
|
+
trace.push(frontier);
|
|
10
|
+
const next = new Map();
|
|
11
|
+
for (let k = -d; k <= d; k += 2) {
|
|
12
|
+
if (++work > 1_000_000)
|
|
13
|
+
return null;
|
|
14
|
+
let x = k === -d || (k !== d && (frontier.get(k - 1) ?? -1) < (frontier.get(k + 1) ?? -1))
|
|
15
|
+
? (frontier.get(k + 1) ?? 0)
|
|
16
|
+
: (frontier.get(k - 1) ?? 0) + 1;
|
|
17
|
+
let y = x - k;
|
|
18
|
+
while (x < a.length && y < b.length) {
|
|
19
|
+
if (++work > 1_000_000)
|
|
20
|
+
return null;
|
|
21
|
+
if (a[x] !== b[y])
|
|
22
|
+
break;
|
|
23
|
+
x++;
|
|
24
|
+
y++;
|
|
25
|
+
}
|
|
26
|
+
next.set(k, x);
|
|
27
|
+
if (x >= a.length && y >= b.length) {
|
|
28
|
+
const edits = [];
|
|
29
|
+
for (let depth = d; depth > 0; depth--) {
|
|
30
|
+
const previous = trace[depth];
|
|
31
|
+
const diagonal = x - y;
|
|
32
|
+
const pk = diagonal === -depth ||
|
|
33
|
+
(diagonal !== depth &&
|
|
34
|
+
(previous.get(diagonal - 1) ?? -1) < (previous.get(diagonal + 1) ?? -1))
|
|
35
|
+
? diagonal + 1
|
|
36
|
+
: diagonal - 1;
|
|
37
|
+
const px = previous.get(pk);
|
|
38
|
+
const py = px - pk;
|
|
39
|
+
while (x > px && y > py) {
|
|
40
|
+
x--;
|
|
41
|
+
y--;
|
|
42
|
+
}
|
|
43
|
+
if (x === px) {
|
|
44
|
+
y--;
|
|
45
|
+
edits.push({ x, line: b[y] });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
x--;
|
|
49
|
+
edits.push({ x });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const operations = [];
|
|
53
|
+
for (const edit of edits.reverse()) {
|
|
54
|
+
let op = operations.at(-1);
|
|
55
|
+
if (!op || edit.x !== op.startLine + op.deleteCount) {
|
|
56
|
+
op = { startLine: edit.x, deleteCount: 0, lines: [] };
|
|
57
|
+
operations.push(op);
|
|
58
|
+
}
|
|
59
|
+
if (edit.line === undefined)
|
|
60
|
+
op.deleteCount++;
|
|
61
|
+
else
|
|
62
|
+
op.lines.push(edit.line);
|
|
63
|
+
}
|
|
64
|
+
return operations.reverse();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
frontier = next;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=terminal-preview-diff.js.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { diffLines } from "./terminal-preview-diff.js";
|
|
3
|
+
import { createTmuxPreviewListener, } from "./tmux-preview-listener.js";
|
|
4
|
+
import { InteractionTransportError } from "./transport.js";
|
|
5
|
+
export class TerminalPreviewService {
|
|
6
|
+
capture;
|
|
7
|
+
listen;
|
|
8
|
+
resolveTarget;
|
|
9
|
+
#states = new Map();
|
|
10
|
+
#identity = randomUUID();
|
|
11
|
+
#closed = false;
|
|
12
|
+
constructor(capture, listen = createTmuxPreviewListener(), resolveTarget) {
|
|
13
|
+
this.capture = capture;
|
|
14
|
+
this.listen = listen;
|
|
15
|
+
this.resolveTarget = resolveTarget;
|
|
16
|
+
}
|
|
17
|
+
subscribe(session, binding, assertOwner, frame, fail, signal) {
|
|
18
|
+
if (this.#closed || signal.aborted)
|
|
19
|
+
throw new DOMException("Aborted", "AbortError");
|
|
20
|
+
const key = JSON.stringify([session, binding.socketPath, binding.paneId]);
|
|
21
|
+
let state = this.#states.get(key);
|
|
22
|
+
if (state && state.binding.sessionName !== binding.sessionName) {
|
|
23
|
+
this.#fail(state, new InteractionTransportError("disconnected", "The pane's tmux session changed"));
|
|
24
|
+
state = undefined;
|
|
25
|
+
}
|
|
26
|
+
const fresh = !state;
|
|
27
|
+
if (!state) {
|
|
28
|
+
state = { key, binding, viewers: new Set(), dirty: true, listening: false, disposed: false };
|
|
29
|
+
this.#states.set(key, state);
|
|
30
|
+
}
|
|
31
|
+
const current = state;
|
|
32
|
+
const close = () => {
|
|
33
|
+
signal.removeEventListener("abort", close);
|
|
34
|
+
current.viewers.delete(viewer);
|
|
35
|
+
if (!current.viewers.size)
|
|
36
|
+
this.#dispose(current);
|
|
37
|
+
};
|
|
38
|
+
const viewer = { assertOwner, frame, fail, close, initialized: false };
|
|
39
|
+
current.viewers.add(viewer);
|
|
40
|
+
signal.addEventListener("abort", close, { once: true });
|
|
41
|
+
try {
|
|
42
|
+
assertOwner();
|
|
43
|
+
if (fresh) {
|
|
44
|
+
current.subscription = this.listen(binding, (pane) => {
|
|
45
|
+
if (!pane || pane === binding.paneId) {
|
|
46
|
+
current.dirty = true;
|
|
47
|
+
this.#schedule(current);
|
|
48
|
+
}
|
|
49
|
+
}, (error) => this.#fail(current, error));
|
|
50
|
+
// A listener may fail synchronously during setup.
|
|
51
|
+
if (current.disposed)
|
|
52
|
+
current.subscription.close();
|
|
53
|
+
}
|
|
54
|
+
void current
|
|
55
|
+
.subscription.ready.then(async () => {
|
|
56
|
+
if (current.disposed || !current.viewers.has(viewer))
|
|
57
|
+
return;
|
|
58
|
+
current.listening = true;
|
|
59
|
+
if (!current.snapshot)
|
|
60
|
+
await this.#capture(current);
|
|
61
|
+
if (current.disposed || !current.viewers.has(viewer))
|
|
62
|
+
return;
|
|
63
|
+
assertOwner();
|
|
64
|
+
if (!viewer.initialized) {
|
|
65
|
+
viewer.initialized = true;
|
|
66
|
+
frame({ type: "snapshot", ...current.snapshot });
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => this.#fail(current, error));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.#fail(current, error);
|
|
73
|
+
}
|
|
74
|
+
return close;
|
|
75
|
+
}
|
|
76
|
+
invalidate() {
|
|
77
|
+
for (const state of this.#states.values()) {
|
|
78
|
+
try {
|
|
79
|
+
this.#assertOwner(state);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.#fail(state, error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
close() {
|
|
87
|
+
this.#closed = true;
|
|
88
|
+
for (const state of this.#states.values())
|
|
89
|
+
this.#fail(state, new DOMException("Aborted", "AbortError"));
|
|
90
|
+
}
|
|
91
|
+
#assertOwner(state) {
|
|
92
|
+
if (state.disposed)
|
|
93
|
+
throw new DOMException("Aborted", "AbortError");
|
|
94
|
+
for (const viewer of state.viewers)
|
|
95
|
+
viewer.assertOwner();
|
|
96
|
+
}
|
|
97
|
+
#schedule(state) {
|
|
98
|
+
if (!state.listening || state.disposed || state.task || state.timer)
|
|
99
|
+
return;
|
|
100
|
+
state.timer = setTimeout(() => {
|
|
101
|
+
state.timer = undefined;
|
|
102
|
+
void this.#capture(state).catch(() => { });
|
|
103
|
+
}, 350);
|
|
104
|
+
}
|
|
105
|
+
#capture(state) {
|
|
106
|
+
if (state.task)
|
|
107
|
+
return state.task;
|
|
108
|
+
clearTimeout(state.timer);
|
|
109
|
+
state.timer = undefined;
|
|
110
|
+
state.dirty = false;
|
|
111
|
+
state.task = (async () => {
|
|
112
|
+
if (this.resolveTarget) {
|
|
113
|
+
const current = await this.resolveTarget(state.binding);
|
|
114
|
+
if (!current || current.sessionName !== state.binding.sessionName)
|
|
115
|
+
throw new InteractionTransportError("disconnected", "The pane no longer exists in its tmux session");
|
|
116
|
+
}
|
|
117
|
+
this.#assertOwner(state);
|
|
118
|
+
const result = await this.capture(state.binding, () => this.#assertOwner(state));
|
|
119
|
+
this.#assertOwner(state);
|
|
120
|
+
const previous = state.snapshot;
|
|
121
|
+
if (previous &&
|
|
122
|
+
previous.content === result.content &&
|
|
123
|
+
previous.truncated === result.truncated)
|
|
124
|
+
return;
|
|
125
|
+
const snapshot = {
|
|
126
|
+
...result,
|
|
127
|
+
capturedAt: new Date().toISOString(),
|
|
128
|
+
revision: createHash("sha256")
|
|
129
|
+
.update(JSON.stringify([this.#identity, state.key, result.content, result.truncated]))
|
|
130
|
+
.digest("base64url"),
|
|
131
|
+
};
|
|
132
|
+
state.snapshot = snapshot;
|
|
133
|
+
const full = { type: "snapshot", ...snapshot };
|
|
134
|
+
let update = full;
|
|
135
|
+
if (previous) {
|
|
136
|
+
const operations = diffLines(previous.content, snapshot.content);
|
|
137
|
+
if (operations !== null) {
|
|
138
|
+
const delta = {
|
|
139
|
+
type: "delta",
|
|
140
|
+
baseRevision: previous.revision,
|
|
141
|
+
revision: snapshot.revision,
|
|
142
|
+
operations,
|
|
143
|
+
truncated: snapshot.truncated,
|
|
144
|
+
capturedAt: snapshot.capturedAt,
|
|
145
|
+
};
|
|
146
|
+
const size = bytes(delta);
|
|
147
|
+
if (size <= 8192 && size < bytes(full))
|
|
148
|
+
update = delta;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const viewer of [...state.viewers]) {
|
|
152
|
+
if (!state.viewers.has(viewer))
|
|
153
|
+
continue;
|
|
154
|
+
viewer.assertOwner();
|
|
155
|
+
const message = viewer.initialized ? update : full;
|
|
156
|
+
viewer.initialized = true;
|
|
157
|
+
viewer.frame(message);
|
|
158
|
+
}
|
|
159
|
+
})()
|
|
160
|
+
.catch((error) => {
|
|
161
|
+
this.#fail(state, error);
|
|
162
|
+
throw error;
|
|
163
|
+
})
|
|
164
|
+
.finally(() => {
|
|
165
|
+
state.task = undefined;
|
|
166
|
+
if (state.dirty)
|
|
167
|
+
this.#schedule(state);
|
|
168
|
+
});
|
|
169
|
+
return state.task;
|
|
170
|
+
}
|
|
171
|
+
#fail(state, error) {
|
|
172
|
+
if (state.disposed)
|
|
173
|
+
return;
|
|
174
|
+
const viewers = [...state.viewers];
|
|
175
|
+
this.#dispose(state);
|
|
176
|
+
for (const viewer of viewers) {
|
|
177
|
+
viewer.close();
|
|
178
|
+
viewer.fail(error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
#dispose(state) {
|
|
182
|
+
if (state.disposed)
|
|
183
|
+
return;
|
|
184
|
+
state.disposed = true;
|
|
185
|
+
clearTimeout(state.timer);
|
|
186
|
+
state.subscription?.close();
|
|
187
|
+
if (this.#states.get(state.key) === state)
|
|
188
|
+
this.#states.delete(state.key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function bytes(value) {
|
|
192
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=terminal-preview-service.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { SessionLaunchError } from "../management/session-manager.js";
|
|
4
|
+
import { TmuxInteractionService } from "./tmux-service.js";
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
export function shellQuote(value) {
|
|
7
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
8
|
+
}
|
|
9
|
+
export function attachCommand(socket, session) {
|
|
10
|
+
return [
|
|
11
|
+
"tmux",
|
|
12
|
+
...(socket ? ["-S", shellQuote(socket)] : []),
|
|
13
|
+
"attach-session",
|
|
14
|
+
"-t",
|
|
15
|
+
shellQuote(session),
|
|
16
|
+
].join(" ");
|
|
17
|
+
}
|
|
18
|
+
/** Launch argv directly; user values are never interpreted by a shell. */
|
|
19
|
+
export class TmuxLauncher {
|
|
20
|
+
socketPath;
|
|
21
|
+
transport = new TmuxInteractionService();
|
|
22
|
+
constructor(socketPath) {
|
|
23
|
+
this.socketPath = socketPath;
|
|
24
|
+
}
|
|
25
|
+
async launch(id, directory, command) {
|
|
26
|
+
const fallbackAttach = attachCommand(this.socketPath, `codex-hub-${id}`);
|
|
27
|
+
let stdout;
|
|
28
|
+
try {
|
|
29
|
+
({ stdout } = await execute("tmux", [
|
|
30
|
+
...(this.socketPath ? ["-S", this.socketPath] : []),
|
|
31
|
+
"new-session",
|
|
32
|
+
"-d",
|
|
33
|
+
"-P",
|
|
34
|
+
"-F",
|
|
35
|
+
"#{socket_path}|#{pane_id}",
|
|
36
|
+
"-s",
|
|
37
|
+
`codex-hub-${id}`,
|
|
38
|
+
"-x",
|
|
39
|
+
"120",
|
|
40
|
+
"-y",
|
|
41
|
+
"40",
|
|
42
|
+
"-c",
|
|
43
|
+
directory,
|
|
44
|
+
...command,
|
|
45
|
+
], { timeout: 5_000 }));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const uncertain = !!error &&
|
|
49
|
+
typeof error === "object" &&
|
|
50
|
+
(("killed" in error && error.killed) ||
|
|
51
|
+
("code" in error && error.code === "timeout") ||
|
|
52
|
+
("signal" in error && error.signal != null));
|
|
53
|
+
throw new SessionLaunchError(uncertain ? "unknown" : "not_started", error instanceof Error ? error.message : String(error), uncertain ? fallbackAttach : null);
|
|
54
|
+
}
|
|
55
|
+
const output = stdout.trimEnd();
|
|
56
|
+
const separator = output.lastIndexOf("|");
|
|
57
|
+
const socketPath = output.slice(0, separator);
|
|
58
|
+
const paneId = output.slice(separator + 1);
|
|
59
|
+
if (separator < 1 ||
|
|
60
|
+
!socketPath.startsWith("/") ||
|
|
61
|
+
socketPath.includes("\n") ||
|
|
62
|
+
!/^%\d+$/.test(paneId)) {
|
|
63
|
+
throw new SessionLaunchError("unknown", "Invalid launched tmux target", fallbackAttach);
|
|
64
|
+
}
|
|
65
|
+
const target = { socketPath, paneId };
|
|
66
|
+
try {
|
|
67
|
+
await execute("tmux", ["-S", socketPath, "set-option", "-p", "-t", paneId, "remain-on-exit", "off"], { timeout: 5_000 });
|
|
68
|
+
return await this.transport.connect({ valid: true, ordinal: 0, ...target });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new SessionLaunchError("unknown", error instanceof Error ? error.message : String(error), attachCommand(socketPath, `codex-hub-${id}`), target);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=tmux-launcher.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** One control client per socket/session; protocol payload is never interpreted as commands. */
|
|
3
|
+
export function createTmuxPreviewListener() {
|
|
4
|
+
const connections = new Map();
|
|
5
|
+
return (binding, change, fail) => {
|
|
6
|
+
const key = JSON.stringify([binding.socketPath, binding.sessionName]);
|
|
7
|
+
let connection = connections.get(key);
|
|
8
|
+
if (!connection) {
|
|
9
|
+
const child = spawn("tmux", [
|
|
10
|
+
"-S",
|
|
11
|
+
binding.socketPath,
|
|
12
|
+
"-C",
|
|
13
|
+
"attach-session",
|
|
14
|
+
"-f",
|
|
15
|
+
// tmux may use this client as the context for separate send commands.
|
|
16
|
+
// Marking it read-only also prevents those commands from writing.
|
|
17
|
+
"ignore-size",
|
|
18
|
+
"-E",
|
|
19
|
+
"-t",
|
|
20
|
+
`=${binding.sessionName}`,
|
|
21
|
+
], { stdio: ["pipe", "pipe", "pipe"] });
|
|
22
|
+
const subscribers = new Set();
|
|
23
|
+
let buffer = "";
|
|
24
|
+
let settled = false;
|
|
25
|
+
let stopped = false;
|
|
26
|
+
let resolveReady;
|
|
27
|
+
let rejectReady;
|
|
28
|
+
const ready = new Promise((resolve, reject) => {
|
|
29
|
+
resolveReady = resolve;
|
|
30
|
+
rejectReady = reject;
|
|
31
|
+
});
|
|
32
|
+
void ready.catch(() => { });
|
|
33
|
+
const timer = setTimeout(() => failure(new Error("tmux listener startup timed out")), 5000);
|
|
34
|
+
function failure(error) {
|
|
35
|
+
if (stopped)
|
|
36
|
+
return;
|
|
37
|
+
stopped = true;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
connections.delete(key);
|
|
40
|
+
rejectReady(error);
|
|
41
|
+
child.kill();
|
|
42
|
+
for (const subscriber of subscribers)
|
|
43
|
+
subscriber.fail(error);
|
|
44
|
+
subscribers.clear();
|
|
45
|
+
}
|
|
46
|
+
child.stdout.setEncoding("utf8");
|
|
47
|
+
child.stdout.on("data", (chunk) => {
|
|
48
|
+
buffer += chunk;
|
|
49
|
+
let end;
|
|
50
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
51
|
+
const line = buffer.slice(0, end).replace(/\r$/, "");
|
|
52
|
+
buffer = buffer.slice(end + 1);
|
|
53
|
+
if (line.startsWith("%end ") && !settled) {
|
|
54
|
+
settled = true;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolveReady();
|
|
57
|
+
}
|
|
58
|
+
if (line.startsWith("%error") || line.startsWith("%exit")) {
|
|
59
|
+
failure(new Error("tmux control connection ended"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const output = /^%(?:output|extended-output) (%\d+) /.exec(line);
|
|
63
|
+
if (output)
|
|
64
|
+
for (const subscriber of subscribers)
|
|
65
|
+
subscriber.change(output[1]);
|
|
66
|
+
else if (/^%(?:layout-change|window-add|window-close|unlinked-window-close|sessions-changed|session-changed|session-window-changed|window-pane-changed|pane-mode-changed)\b/.test(line))
|
|
67
|
+
for (const subscriber of subscribers)
|
|
68
|
+
subscriber.change();
|
|
69
|
+
}
|
|
70
|
+
if (buffer.length > 2_000_000)
|
|
71
|
+
failure(new Error("tmux control line exceeds limit"));
|
|
72
|
+
});
|
|
73
|
+
child.stderr.resume();
|
|
74
|
+
child.on("error", failure);
|
|
75
|
+
child.stdin.on("error", failure);
|
|
76
|
+
child.on("close", () => failure(new Error("tmux control connection closed")));
|
|
77
|
+
connection = {
|
|
78
|
+
subscribe(change, fail) {
|
|
79
|
+
const subscriber = { change, fail };
|
|
80
|
+
subscribers.add(subscriber);
|
|
81
|
+
return {
|
|
82
|
+
ready,
|
|
83
|
+
close() {
|
|
84
|
+
subscribers.delete(subscriber);
|
|
85
|
+
if (!subscribers.size && !stopped) {
|
|
86
|
+
stopped = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
rejectReady(new Error("tmux control subscription closed"));
|
|
89
|
+
connections.delete(key);
|
|
90
|
+
child.kill();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
connections.set(key, connection);
|
|
97
|
+
}
|
|
98
|
+
return connection.subscribe(change, fail);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=tmux-preview-listener.js.map
|