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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Run and manage AI agent sessions locally, with optional relay to agentoffice.top",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -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
- if (text.includes("approval") || text.includes("press enter") || text.includes("confirm")) {
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
- 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
- ) {
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
- // PTY sessions stream via broadcastTerminal, no per-client attach needed
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) {