agent-office-cli 0.1.1 → 0.1.3
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
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,31 @@
|
|
|
1
1
|
const { GenericProvider } = require("./generic");
|
|
2
2
|
const { findManagedCodexSessionFile, summarizeCodexSession } = require("./codex-transcript");
|
|
3
3
|
|
|
4
|
+
const APPROVAL_PATTERNS = [
|
|
5
|
+
"approval requested:",
|
|
6
|
+
"approval requested by ",
|
|
7
|
+
"tool call needs your approval",
|
|
8
|
+
"requires approval by policy",
|
|
9
|
+
"requires approval:"
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const IDLE_PATTERNS = [
|
|
13
|
+
"conversation interrupted - tell the model what to do differently",
|
|
14
|
+
"something went wrong? hit `/feedback` to",
|
|
15
|
+
"something went wrong? hit /feedback to"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const ATTENTION_PATTERNS = [
|
|
19
|
+
"stream disconnected before completion",
|
|
20
|
+
"error sending request for url",
|
|
21
|
+
"network error",
|
|
22
|
+
"connection timeout",
|
|
23
|
+
"timed out",
|
|
24
|
+
"failed to send request",
|
|
25
|
+
"failed to submit",
|
|
26
|
+
"panic"
|
|
27
|
+
];
|
|
28
|
+
|
|
4
29
|
function activeOverlayPatch(session, nextLifecycleState) {
|
|
5
30
|
if (!session || !["approval", "attention"].includes(session.displayState)) {
|
|
6
31
|
return null;
|
|
@@ -48,19 +73,19 @@ class CodexProvider extends GenericProvider {
|
|
|
48
73
|
|
|
49
74
|
classifyOutput(chunk) {
|
|
50
75
|
const text = String(chunk).toLowerCase();
|
|
51
|
-
|
|
76
|
+
|
|
77
|
+
if (IDLE_PATTERNS.some((pattern) => text.includes(pattern))) {
|
|
78
|
+
return "idle";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (APPROVAL_PATTERNS.some((pattern) => text.includes(pattern))) {
|
|
52
82
|
return "approval";
|
|
53
83
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
text.includes("connection timeout") ||
|
|
57
|
-
text.includes("timed out") ||
|
|
58
|
-
text.includes("error") ||
|
|
59
|
-
text.includes("failed") ||
|
|
60
|
-
text.includes("panic")
|
|
61
|
-
) {
|
|
84
|
+
|
|
85
|
+
if (ATTENTION_PATTERNS.some((pattern) => text.includes(pattern))) {
|
|
62
86
|
return "attention";
|
|
63
87
|
}
|
|
88
|
+
|
|
64
89
|
return null;
|
|
65
90
|
}
|
|
66
91
|
|
|
@@ -72,3 +72,39 @@ test("classifyOutput can raise attention for transcript-backed sessions", () =>
|
|
|
72
72
|
|
|
73
73
|
assert.equal(nextState, "attention");
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
test("classifyOutput treats user interrupted Codex screens as idle", () => {
|
|
77
|
+
const provider = new CodexProvider();
|
|
78
|
+
const nextState = provider.classifyOutput(
|
|
79
|
+
"Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue."
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
assert.equal(nextState, "idle");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("classifyOutput treats stream disconnects as attention", () => {
|
|
86
|
+
const provider = new CodexProvider();
|
|
87
|
+
const nextState = provider.classifyOutput(
|
|
88
|
+
"stream disconnected before completion: error sending request for url (http://54.255.64.152:3000/openai/responses)"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
assert.equal(nextState, "attention");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("classifyOutput does not treat plain explanatory approval text as a real approval prompt", () => {
|
|
95
|
+
const provider = new CodexProvider();
|
|
96
|
+
const nextState = provider.classifyOutput(
|
|
97
|
+
"只有真实审批提示才归到 approval"
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
assert.equal(nextState, null);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("classifyOutput recognizes real Codex approval prompts", () => {
|
|
104
|
+
const provider = new CodexProvider();
|
|
105
|
+
const nextState = provider.classifyOutput(
|
|
106
|
+
"Approval requested: Codex wants to edit files"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assert.equal(nextState, "approval");
|
|
110
|
+
});
|
|
@@ -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
|
|
|
@@ -53,7 +53,8 @@ function createSessionStore() {
|
|
|
53
53
|
host: payload.host || os.hostname(),
|
|
54
54
|
meta: { ...(payload.meta || {}) },
|
|
55
55
|
logs: [...(payload.logs || [])],
|
|
56
|
-
events: [...(payload.events || [])]
|
|
56
|
+
events: [...(payload.events || [])],
|
|
57
|
+
terminalReplay: String(payload.terminalReplay || "")
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -151,6 +152,10 @@ function createSessionStore() {
|
|
|
151
152
|
const lines = String(chunk).replace(/\r/g, "").split("\n").filter(Boolean);
|
|
152
153
|
session.logs.push(...lines);
|
|
153
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
|
+
}
|
|
154
159
|
session.lastOutputAt = isoNow();
|
|
155
160
|
session.updatedAt = session.lastOutputAt;
|
|
156
161
|
// Terminal output does not change session state — skip session:update broadcast.
|
|
@@ -189,6 +194,11 @@ function createSessionStore() {
|
|
|
189
194
|
return session ? toSessionSummary(session) : null;
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
function getTerminalReplay(sessionId) {
|
|
198
|
+
const session = sessions.get(sessionId);
|
|
199
|
+
return session ? session.terminalReplay || "" : "";
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
function listSessions() {
|
|
193
203
|
return [...sessions.values()]
|
|
194
204
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
@@ -224,6 +234,7 @@ function createSessionStore() {
|
|
|
224
234
|
markExit,
|
|
225
235
|
getSession,
|
|
226
236
|
getSessionSummary,
|
|
237
|
+
getTerminalReplay,
|
|
227
238
|
listSessions,
|
|
228
239
|
listSessionSummaries,
|
|
229
240
|
removeSession
|
|
@@ -48,3 +48,21 @@ test("setSessionState derives displayZone from displayState override when zone i
|
|
|
48
48
|
assert.equal(next.displayState, "attention");
|
|
49
49
|
assert.equal(next.displayZone, "attention-zone");
|
|
50
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
|
+
});
|
|
@@ -503,18 +503,25 @@ function createPtyManager({ store }) {
|
|
|
503
503
|
|
|
504
504
|
let attachedClient = null;
|
|
505
505
|
let tmuxStreamStarted = false;
|
|
506
|
+
let pendingCols = 120;
|
|
507
|
+
let pendingRows = 32;
|
|
506
508
|
|
|
507
509
|
async function startTmuxStream(cols, rows) {
|
|
508
510
|
if (tmuxStreamStarted || !entry || entry.transport !== "tmux") {
|
|
509
511
|
return;
|
|
510
512
|
}
|
|
511
513
|
tmuxStreamStarted = true;
|
|
514
|
+
pendingCols = cols;
|
|
515
|
+
pendingRows = rows;
|
|
512
516
|
try {
|
|
513
517
|
const snapshot = await capturePane(entry.tmuxSession);
|
|
514
518
|
if (snapshot && ws.readyState === 1) {
|
|
515
519
|
ws.send(JSON.stringify({ type: "terminal:data", data: `${snapshot}\r\n` }));
|
|
516
520
|
}
|
|
517
521
|
attachedClient = attachClient(entry.tmuxSession, { cwd: entry.cwd, cols, rows });
|
|
522
|
+
if (pendingCols !== cols || pendingRows !== rows) {
|
|
523
|
+
attachedClient.resize(pendingCols, pendingRows);
|
|
524
|
+
}
|
|
518
525
|
attachedClient.onData((chunk) => {
|
|
519
526
|
if (ws.readyState === 1) {
|
|
520
527
|
ws.send(JSON.stringify({ type: "terminal:data", data: chunk }));
|
|
@@ -532,7 +539,12 @@ function createPtyManager({ store }) {
|
|
|
532
539
|
|
|
533
540
|
// For non-tmux transports that had immediate setup, keep original behavior
|
|
534
541
|
if (entry && entry.transport === "pty") {
|
|
535
|
-
|
|
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);
|
|
536
548
|
}
|
|
537
549
|
|
|
538
550
|
ws.on("message", async (raw) => {
|
|
@@ -553,6 +565,8 @@ function createPtyManager({ store }) {
|
|
|
553
565
|
if (message.type === "resize") {
|
|
554
566
|
const cols = Number(message.cols || 120);
|
|
555
567
|
const rows = Number(message.rows || 32);
|
|
568
|
+
pendingCols = cols;
|
|
569
|
+
pendingRows = rows;
|
|
556
570
|
if (runtime.transport === "pty") {
|
|
557
571
|
runtime.pty.resize(cols, rows);
|
|
558
572
|
} else if (!tmuxStreamStarted) {
|