agent-office-cli 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/core/config.js +3 -1
- package/src/core/providers/codex.js +57 -11
- package/src/core/providers/codex.test.js +74 -0
- package/src/core/session-contract.js +3 -1
- package/src/core/session-contract.test.js +27 -0
- package/src/core/store/session-store.js +32 -18
- package/src/core/store/session-store.test.js +68 -0
- package/src/runtime/postinstall-path.test.js +26 -0
- package/src/runtime/pty-manager.js +30 -15
- package/src/server.js +17 -0
package/package.json
CHANGED
package/src/core/config.js
CHANGED
|
@@ -3,11 +3,13 @@ const DEFAULT_LAN_HOST = "0.0.0.0";
|
|
|
3
3
|
const DEFAULT_PORT = 8765;
|
|
4
4
|
const DEFAULT_SERVER_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
|
5
5
|
const LOG_LIMIT = 1000;
|
|
6
|
+
const TERMINAL_REPLAY_LIMIT = 128 * 1024;
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
DEFAULT_HOST,
|
|
9
10
|
DEFAULT_LAN_HOST,
|
|
10
11
|
DEFAULT_PORT,
|
|
11
12
|
DEFAULT_SERVER_URL,
|
|
12
|
-
LOG_LIMIT
|
|
13
|
+
LOG_LIMIT,
|
|
14
|
+
TERMINAL_REPLAY_LIMIT
|
|
13
15
|
};
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
const { GenericProvider } = require("./generic");
|
|
2
2
|
const { findManagedCodexSessionFile, summarizeCodexSession } = require("./codex-transcript");
|
|
3
3
|
|
|
4
|
+
function activeOverlayPatch(session, nextLifecycleState) {
|
|
5
|
+
if (!session || !["approval", "attention"].includes(session.displayState)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (nextLifecycleState === "idle") {
|
|
10
|
+
return {
|
|
11
|
+
displayState: "idle",
|
|
12
|
+
displayZone: "idle-zone",
|
|
13
|
+
meta: {
|
|
14
|
+
overlayState: null,
|
|
15
|
+
overlayUpdatedAt: null
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
displayState: session.displayState,
|
|
22
|
+
displayZone: session.displayZone,
|
|
23
|
+
meta: {
|
|
24
|
+
overlayState: session.displayState,
|
|
25
|
+
overlayUpdatedAt: session.updatedAt || null
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
class CodexProvider extends GenericProvider {
|
|
5
31
|
constructor() {
|
|
6
32
|
super("codex");
|
|
@@ -25,7 +51,14 @@ class CodexProvider extends GenericProvider {
|
|
|
25
51
|
if (text.includes("approval") || text.includes("press enter") || text.includes("confirm")) {
|
|
26
52
|
return "approval";
|
|
27
53
|
}
|
|
28
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
text.includes("network error") ||
|
|
56
|
+
text.includes("connection timeout") ||
|
|
57
|
+
text.includes("timed out") ||
|
|
58
|
+
text.includes("error") ||
|
|
59
|
+
text.includes("failed") ||
|
|
60
|
+
text.includes("panic")
|
|
61
|
+
) {
|
|
29
62
|
return "attention";
|
|
30
63
|
}
|
|
31
64
|
return null;
|
|
@@ -50,7 +83,8 @@ class CodexProvider extends GenericProvider {
|
|
|
50
83
|
};
|
|
51
84
|
|
|
52
85
|
const previousPath = session.meta && session.meta.codexSessionPath;
|
|
53
|
-
const
|
|
86
|
+
const overlay = activeOverlayPatch(session, summary.state);
|
|
87
|
+
const previousState = session.state;
|
|
54
88
|
const previousCursor = session.meta && session.meta.codexTranscriptCursor;
|
|
55
89
|
const previousLifecycle = session.meta && session.meta.codexLastLifecycle;
|
|
56
90
|
const lifecycleAdvanced = Boolean(summary.lastTimestamp && summary.lastTimestamp !== previousCursor);
|
|
@@ -68,20 +102,32 @@ class CodexProvider extends GenericProvider {
|
|
|
68
102
|
return {
|
|
69
103
|
session: metaChanged
|
|
70
104
|
? {
|
|
71
|
-
|
|
105
|
+
meta: nextMeta
|
|
72
106
|
}
|
|
73
107
|
: null,
|
|
74
108
|
state: summary.state,
|
|
75
|
-
patch: summary.state
|
|
76
|
-
eventName: lifecycleAdvanced && summary.lastLifecycle ? `codex_${summary.lastLifecycle}` : null,
|
|
77
|
-
meta: lifecycleAdvanced
|
|
109
|
+
patch: summary.state
|
|
78
110
|
? {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
status: "running",
|
|
112
|
+
...(overlay
|
|
113
|
+
? {
|
|
114
|
+
displayState: overlay.displayState,
|
|
115
|
+
displayZone: overlay.displayZone
|
|
116
|
+
}
|
|
117
|
+
: {})
|
|
83
118
|
}
|
|
84
|
-
: null
|
|
119
|
+
: null,
|
|
120
|
+
eventName: lifecycleAdvanced && summary.lastLifecycle ? `codex_${summary.lastLifecycle}` : null,
|
|
121
|
+
meta:
|
|
122
|
+
lifecycleAdvanced || overlay
|
|
123
|
+
? {
|
|
124
|
+
codexSessionPath: matched.path,
|
|
125
|
+
codexSessionId: matched.sessionMeta && matched.sessionMeta.id,
|
|
126
|
+
turnId: summary.lastTurnId || null,
|
|
127
|
+
lastAgentMessage: summary.lastAgentMessage || null,
|
|
128
|
+
...(overlay ? overlay.meta : {})
|
|
129
|
+
}
|
|
130
|
+
: null
|
|
85
131
|
};
|
|
86
132
|
}
|
|
87
133
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
const assert = require("node:assert/strict");
|
|
6
|
+
|
|
7
|
+
const { CodexProvider } = require("./codex");
|
|
8
|
+
|
|
9
|
+
function writeTranscriptFile(entries) {
|
|
10
|
+
const dirPath = fs.mkdtempSync(path.join(os.tmpdir(), "codex-transcript-"));
|
|
11
|
+
const filePath = path.join(dirPath, "session.jsonl");
|
|
12
|
+
fs.writeFileSync(
|
|
13
|
+
filePath,
|
|
14
|
+
`${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
|
|
15
|
+
"utf8"
|
|
16
|
+
);
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("reconcileSession keeps approval display overlay when transcript advances to working", () => {
|
|
21
|
+
const transcriptPath = writeTranscriptFile([
|
|
22
|
+
{
|
|
23
|
+
type: "session_meta",
|
|
24
|
+
payload: {
|
|
25
|
+
id: "codex-session-1",
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
timestamp: "2026-03-18T10:00:00.000Z"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
timestamp: "2026-03-18T10:00:01.000Z",
|
|
32
|
+
type: "event_msg",
|
|
33
|
+
payload: {
|
|
34
|
+
type: "task_started",
|
|
35
|
+
turn_id: "turn-1"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const provider = new CodexProvider();
|
|
41
|
+
const session = provider.createSession({
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
title: "Codex",
|
|
44
|
+
command: "codex",
|
|
45
|
+
meta: {
|
|
46
|
+
codexSessionPath: transcriptPath,
|
|
47
|
+
codexTranscriptCursor: null,
|
|
48
|
+
codexLastLifecycle: null
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
session.status = "running";
|
|
52
|
+
session.state = "working";
|
|
53
|
+
session.displayState = "approval";
|
|
54
|
+
session.displayZone = "approval-zone";
|
|
55
|
+
session.updatedAt = "2026-03-18T10:00:02.000Z";
|
|
56
|
+
|
|
57
|
+
const result = provider.reconcileSession(session, { sessions: [session] });
|
|
58
|
+
|
|
59
|
+
assert.ok(result);
|
|
60
|
+
assert.equal(result.state, "working");
|
|
61
|
+
assert.equal(result.patch.displayState, "approval");
|
|
62
|
+
assert.equal(result.patch.displayZone, "approval-zone");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("classifyOutput can raise attention for transcript-backed sessions", () => {
|
|
66
|
+
const provider = new CodexProvider();
|
|
67
|
+
const nextState = provider.classifyOutput("network error: connection timed out", {
|
|
68
|
+
meta: {
|
|
69
|
+
codexSessionPath: "/tmp/mock-codex.jsonl"
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(nextState, "attention");
|
|
74
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
const { displayZoneFor } = require("./state");
|
|
2
|
+
|
|
1
3
|
const CONTRACT_VERSION = 1;
|
|
2
4
|
|
|
3
5
|
function sessionLifecycle(session) {
|
|
4
6
|
const status = session.status || "registered";
|
|
5
7
|
const displayState = session.displayState || session.state || "idle";
|
|
6
|
-
const displayZone = session.displayZone ||
|
|
8
|
+
const displayZone = session.displayZone || displayZoneFor(displayState);
|
|
7
9
|
|
|
8
10
|
return {
|
|
9
11
|
status,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
|
|
4
|
+
const { toPublicSession } = require("./session-contract");
|
|
5
|
+
|
|
6
|
+
test("toPublicSession derives displayZone from displayState when zone is missing", () => {
|
|
7
|
+
const session = toPublicSession({
|
|
8
|
+
sessionId: "sess_3",
|
|
9
|
+
provider: "codex",
|
|
10
|
+
title: "Codex",
|
|
11
|
+
command: "codex",
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
mode: "managed",
|
|
14
|
+
transport: "tmux",
|
|
15
|
+
state: "working",
|
|
16
|
+
displayState: "approval",
|
|
17
|
+
status: "running",
|
|
18
|
+
createdAt: "2026-03-18T00:00:00.000Z",
|
|
19
|
+
updatedAt: "2026-03-18T00:00:01.000Z",
|
|
20
|
+
meta: {}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.equal(session.state, "working");
|
|
24
|
+
assert.equal(session.displayState, "approval");
|
|
25
|
+
assert.equal(session.displayZone, "approval-zone");
|
|
26
|
+
assert.equal(session.lifecycle.displayZone, "approval-zone");
|
|
27
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require("node:events");
|
|
2
2
|
const os = require("node:os");
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
|
-
const { LOG_LIMIT } = require("../config");
|
|
4
|
+
const { LOG_LIMIT, TERMINAL_REPLAY_LIMIT } = require("../config");
|
|
5
5
|
const { displayZoneFor } = require("../state");
|
|
6
6
|
const { toPublicSession, toSessionSummary } = require("../session-contract");
|
|
7
7
|
|
|
@@ -13,10 +13,14 @@ function cloneSession(session) {
|
|
|
13
13
|
return toPublicSession(session);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function applyDisplay(session, nextDisplayState, nextDisplayZone = null) {
|
|
17
|
+
session.displayState = nextDisplayState;
|
|
18
|
+
session.displayZone = nextDisplayZone || displayZoneFor(nextDisplayState);
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
function applyState(session, nextState) {
|
|
17
22
|
session.state = nextState;
|
|
18
|
-
session
|
|
19
|
-
session.displayZone = displayZoneFor(nextState);
|
|
23
|
+
applyDisplay(session, nextState);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
function createSessionStore() {
|
|
@@ -26,6 +30,7 @@ function createSessionStore() {
|
|
|
26
30
|
function buildSession(payload) {
|
|
27
31
|
const sessionId = payload.sessionId || `sess_${crypto.randomBytes(5).toString("hex")}`;
|
|
28
32
|
const state = payload.state || "idle";
|
|
33
|
+
const displayState = payload.displayState || state;
|
|
29
34
|
const createdAt = payload.createdAt || isoNow();
|
|
30
35
|
const updatedAt = payload.updatedAt || createdAt;
|
|
31
36
|
|
|
@@ -38,8 +43,8 @@ function createSessionStore() {
|
|
|
38
43
|
mode: payload.mode || "managed",
|
|
39
44
|
transport: payload.transport || "pty",
|
|
40
45
|
state,
|
|
41
|
-
displayState
|
|
42
|
-
displayZone: payload.displayZone || displayZoneFor(
|
|
46
|
+
displayState,
|
|
47
|
+
displayZone: payload.displayZone || displayZoneFor(displayState),
|
|
43
48
|
status: payload.status || "registered",
|
|
44
49
|
createdAt,
|
|
45
50
|
updatedAt,
|
|
@@ -48,7 +53,8 @@ function createSessionStore() {
|
|
|
48
53
|
host: payload.host || os.hostname(),
|
|
49
54
|
meta: { ...(payload.meta || {}) },
|
|
50
55
|
logs: [...(payload.logs || [])],
|
|
51
|
-
events: [...(payload.events || [])]
|
|
56
|
+
events: [...(payload.events || [])],
|
|
57
|
+
terminalReplay: String(payload.terminalReplay || "")
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -86,10 +92,10 @@ function createSessionStore() {
|
|
|
86
92
|
if (payload.state) {
|
|
87
93
|
applyState(existing, payload.state);
|
|
88
94
|
}
|
|
89
|
-
if (payload.displayState
|
|
90
|
-
existing.displayState
|
|
95
|
+
if (payload.displayState) {
|
|
96
|
+
applyDisplay(existing, payload.displayState, payload.displayZone);
|
|
91
97
|
}
|
|
92
|
-
if (payload.displayZone && !payload.
|
|
98
|
+
if (payload.displayZone && !payload.displayState) {
|
|
93
99
|
existing.displayZone = payload.displayZone;
|
|
94
100
|
}
|
|
95
101
|
if (payload.status) {
|
|
@@ -110,9 +116,9 @@ function createSessionStore() {
|
|
|
110
116
|
session.updatedAt = isoNow();
|
|
111
117
|
Object.assign(session, patch);
|
|
112
118
|
if (patch.displayState) {
|
|
113
|
-
session.displayState
|
|
119
|
+
applyDisplay(session, patch.displayState, patch.displayZone);
|
|
114
120
|
}
|
|
115
|
-
if (patch.displayZone) {
|
|
121
|
+
if (patch.displayZone && !patch.displayState) {
|
|
116
122
|
session.displayZone = patch.displayZone;
|
|
117
123
|
}
|
|
118
124
|
emitUpdate(sessionId);
|
|
@@ -146,6 +152,10 @@ function createSessionStore() {
|
|
|
146
152
|
const lines = String(chunk).replace(/\r/g, "").split("\n").filter(Boolean);
|
|
147
153
|
session.logs.push(...lines);
|
|
148
154
|
session.logs = session.logs.slice(-LOG_LIMIT);
|
|
155
|
+
session.terminalReplay = `${session.terminalReplay || ""}${String(chunk)}`;
|
|
156
|
+
if (session.terminalReplay.length > TERMINAL_REPLAY_LIMIT) {
|
|
157
|
+
session.terminalReplay = session.terminalReplay.slice(-TERMINAL_REPLAY_LIMIT);
|
|
158
|
+
}
|
|
149
159
|
session.lastOutputAt = isoNow();
|
|
150
160
|
session.updatedAt = session.lastOutputAt;
|
|
151
161
|
// Terminal output does not change session state — skip session:update broadcast.
|
|
@@ -164,13 +174,11 @@ function createSessionStore() {
|
|
|
164
174
|
Object.assign(session, patch);
|
|
165
175
|
if (patch.state) {
|
|
166
176
|
applyState(session, patch.state);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
session.displayZone = patch.displayZone;
|
|
173
|
-
}
|
|
177
|
+
}
|
|
178
|
+
if (patch.displayState) {
|
|
179
|
+
applyDisplay(session, patch.displayState, patch.displayZone);
|
|
180
|
+
} else if (patch.displayZone) {
|
|
181
|
+
session.displayZone = patch.displayZone;
|
|
174
182
|
}
|
|
175
183
|
emitUpdate(sessionId);
|
|
176
184
|
return session;
|
|
@@ -186,6 +194,11 @@ function createSessionStore() {
|
|
|
186
194
|
return session ? toSessionSummary(session) : null;
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
function getTerminalReplay(sessionId) {
|
|
198
|
+
const session = sessions.get(sessionId);
|
|
199
|
+
return session ? session.terminalReplay || "" : "";
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
function listSessions() {
|
|
190
203
|
return [...sessions.values()]
|
|
191
204
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
@@ -221,6 +234,7 @@ function createSessionStore() {
|
|
|
221
234
|
markExit,
|
|
222
235
|
getSession,
|
|
223
236
|
getSessionSummary,
|
|
237
|
+
getTerminalReplay,
|
|
224
238
|
listSessions,
|
|
225
239
|
listSessionSummaries,
|
|
226
240
|
removeSession
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
|
|
4
|
+
const { createSessionStore } = require("./session-store");
|
|
5
|
+
|
|
6
|
+
test("setSessionState keeps lifecycle state and applies display override", () => {
|
|
7
|
+
const store = createSessionStore();
|
|
8
|
+
store.upsertSession({
|
|
9
|
+
sessionId: "sess_1",
|
|
10
|
+
provider: "codex",
|
|
11
|
+
title: "Codex",
|
|
12
|
+
command: "codex",
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
state: "idle",
|
|
15
|
+
status: "running"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const next = store.setSessionState("sess_1", "working", {
|
|
19
|
+
status: "running",
|
|
20
|
+
displayState: "approval",
|
|
21
|
+
displayZone: "approval-zone"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
assert.equal(next.state, "working");
|
|
25
|
+
assert.equal(next.displayState, "approval");
|
|
26
|
+
assert.equal(next.displayZone, "approval-zone");
|
|
27
|
+
assert.equal(next.status, "running");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("setSessionState derives displayZone from displayState override when zone is omitted", () => {
|
|
31
|
+
const store = createSessionStore();
|
|
32
|
+
store.upsertSession({
|
|
33
|
+
sessionId: "sess_2",
|
|
34
|
+
provider: "codex",
|
|
35
|
+
title: "Codex",
|
|
36
|
+
command: "codex",
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
state: "idle",
|
|
39
|
+
status: "running"
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const next = store.setSessionState("sess_2", "working", {
|
|
43
|
+
status: "running",
|
|
44
|
+
displayState: "attention"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(next.state, "working");
|
|
48
|
+
assert.equal(next.displayState, "attention");
|
|
49
|
+
assert.equal(next.displayZone, "attention-zone");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("appendOutput keeps a raw terminal replay buffer for fast reconnects", () => {
|
|
53
|
+
const store = createSessionStore();
|
|
54
|
+
store.upsertSession({
|
|
55
|
+
sessionId: "sess_3",
|
|
56
|
+
provider: "generic",
|
|
57
|
+
title: "Shell",
|
|
58
|
+
command: "bash",
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
state: "working",
|
|
61
|
+
status: "running"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
store.appendOutput("sess_3", "line one\r\n");
|
|
65
|
+
store.appendOutput("sess_3", "line two\n");
|
|
66
|
+
|
|
67
|
+
assert.equal(store.getTerminalReplay("sess_3"), "line one\r\nline two\n");
|
|
68
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
|
|
6
|
+
test("root postinstall points at the live ensure-node-pty module", () => {
|
|
7
|
+
const rootDir = path.resolve(__dirname, "../../../../");
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
9
|
+
const postinstall = packageJson.scripts && packageJson.scripts.postinstall;
|
|
10
|
+
|
|
11
|
+
assert.match(postinstall, /packages\/cli\/src\/runtime\/ensure-node-pty/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("service packages no longer depend on @agent-office/core", () => {
|
|
15
|
+
const rootDir = path.resolve(__dirname, "../../../../");
|
|
16
|
+
const apiPackageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "services/api/package.json"), "utf8"));
|
|
17
|
+
const relayPackageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "services/relay/package.json"), "utf8"));
|
|
18
|
+
|
|
19
|
+
assert.ok(!("@agent-office/core" in (apiPackageJson.dependencies || {})));
|
|
20
|
+
assert.ok(!("@agent-office/core" in (relayPackageJson.dependencies || {})));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("legacy packages/core package has been removed", () => {
|
|
24
|
+
const rootDir = path.resolve(__dirname, "../../../../");
|
|
25
|
+
assert.equal(fs.existsSync(path.join(rootDir, "packages/core")), false);
|
|
26
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const crypto = require("node:crypto");
|
|
2
2
|
const os = require("node:os");
|
|
3
3
|
const pty = require("node-pty");
|
|
4
|
-
const { getProvider } = require("../core");
|
|
4
|
+
const { displayZoneFor, getProvider } = require("../core");
|
|
5
5
|
const {
|
|
6
6
|
AGENTOFFICE_TMUX_PREFIX,
|
|
7
7
|
attachClient,
|
|
@@ -350,20 +350,21 @@ function createPtyManager({ store }) {
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
const provider = getProvider(session.provider);
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
store.setSessionState(session.sessionId, nextState, { status: "running" });
|
|
364
|
-
}
|
|
353
|
+
const screen = await capturePane(runtime.tmuxSession);
|
|
354
|
+
const latestSession = store.getSession(session.sessionId) || session;
|
|
355
|
+
const overlayState = runtime.provider.classifyOutput(screen, latestSession);
|
|
356
|
+
|
|
357
|
+
if (overlayState && overlayState !== latestSession.displayState) {
|
|
358
|
+
store.setSessionState(session.sessionId, latestSession.state || "working", {
|
|
359
|
+
status: "running",
|
|
360
|
+
displayState: overlayState,
|
|
361
|
+
displayZone: displayZoneFor(overlayState)
|
|
362
|
+
});
|
|
365
363
|
}
|
|
366
|
-
|
|
364
|
+
|
|
365
|
+
const reconciledSession = store.getSession(session.sessionId) || session;
|
|
366
|
+
const reconcileResult = provider.reconcileSession(reconciledSession, { sessions: currentSessions });
|
|
367
|
+
applyProviderReconcile(reconciledSession, reconcileResult);
|
|
367
368
|
continue;
|
|
368
369
|
}
|
|
369
370
|
|
|
@@ -502,18 +503,25 @@ function createPtyManager({ store }) {
|
|
|
502
503
|
|
|
503
504
|
let attachedClient = null;
|
|
504
505
|
let tmuxStreamStarted = false;
|
|
506
|
+
let pendingCols = 120;
|
|
507
|
+
let pendingRows = 32;
|
|
505
508
|
|
|
506
509
|
async function startTmuxStream(cols, rows) {
|
|
507
510
|
if (tmuxStreamStarted || !entry || entry.transport !== "tmux") {
|
|
508
511
|
return;
|
|
509
512
|
}
|
|
510
513
|
tmuxStreamStarted = true;
|
|
514
|
+
pendingCols = cols;
|
|
515
|
+
pendingRows = rows;
|
|
511
516
|
try {
|
|
512
517
|
const snapshot = await capturePane(entry.tmuxSession);
|
|
513
518
|
if (snapshot && ws.readyState === 1) {
|
|
514
519
|
ws.send(JSON.stringify({ type: "terminal:data", data: `${snapshot}\r\n` }));
|
|
515
520
|
}
|
|
516
521
|
attachedClient = attachClient(entry.tmuxSession, { cwd: entry.cwd, cols, rows });
|
|
522
|
+
if (pendingCols !== cols || pendingRows !== rows) {
|
|
523
|
+
attachedClient.resize(pendingCols, pendingRows);
|
|
524
|
+
}
|
|
517
525
|
attachedClient.onData((chunk) => {
|
|
518
526
|
if (ws.readyState === 1) {
|
|
519
527
|
ws.send(JSON.stringify({ type: "terminal:data", data: chunk }));
|
|
@@ -531,7 +539,12 @@ function createPtyManager({ store }) {
|
|
|
531
539
|
|
|
532
540
|
// For non-tmux transports that had immediate setup, keep original behavior
|
|
533
541
|
if (entry && entry.transport === "pty") {
|
|
534
|
-
|
|
542
|
+
const replay = store.getTerminalReplay(sessionId);
|
|
543
|
+
if (replay) {
|
|
544
|
+
ws.send(JSON.stringify({ type: "terminal:data", data: replay }));
|
|
545
|
+
}
|
|
546
|
+
} else if (entry && entry.transport === "tmux") {
|
|
547
|
+
void startTmuxStream(120, 32);
|
|
535
548
|
}
|
|
536
549
|
|
|
537
550
|
ws.on("message", async (raw) => {
|
|
@@ -552,6 +565,8 @@ function createPtyManager({ store }) {
|
|
|
552
565
|
if (message.type === "resize") {
|
|
553
566
|
const cols = Number(message.cols || 120);
|
|
554
567
|
const rows = Number(message.rows || 32);
|
|
568
|
+
pendingCols = cols;
|
|
569
|
+
pendingRows = rows;
|
|
555
570
|
if (runtime.transport === "pty") {
|
|
556
571
|
runtime.pty.resize(cols, rows);
|
|
557
572
|
} else if (!tmuxStreamStarted) {
|
package/src/server.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
1
2
|
const http = require("node:http");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
2
5
|
const express = require("express");
|
|
3
6
|
const { WebSocketServer } = require("ws");
|
|
4
7
|
const auth = require("./auth");
|
|
@@ -112,6 +115,20 @@ function createAppServer({ host, port, store, ptyManager }) {
|
|
|
112
115
|
res.json({ ok: true });
|
|
113
116
|
});
|
|
114
117
|
|
|
118
|
+
// Returns home dir + subdirectories of a given path (for working directory picker)
|
|
119
|
+
app.get("/api/dirs", (req, res) => {
|
|
120
|
+
const home = os.homedir();
|
|
121
|
+
const target = String(req.query.path || home);
|
|
122
|
+
let entries = [];
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(target, { withFileTypes: true })
|
|
125
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
126
|
+
.map((e) => path.join(target, e.name))
|
|
127
|
+
.slice(0, 50);
|
|
128
|
+
} catch { /* unreadable dir */ }
|
|
129
|
+
res.json({ home, path: target, dirs: entries });
|
|
130
|
+
});
|
|
131
|
+
|
|
115
132
|
app.get("/api/sessions", (_req, res) => {
|
|
116
133
|
res.json({ sessions: store.listSessionSummaries() });
|
|
117
134
|
});
|