copilotoffice 2.0.0 → 2.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.
@@ -34,11 +34,131 @@ __export(ipc_relay_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(ipc_relay_exports);
36
36
  var import_electron = require("electron");
37
+ var import_child_process2 = require("child_process");
38
+ var import_events = require("events");
39
+ var crypto2 = __toESM(require("crypto"));
40
+ var path2 = __toESM(require("path"));
41
+
42
+ // electron/terminal/pty-registry.ts
37
43
  var import_child_process = require("child_process");
38
44
  var crypto = __toESM(require("crypto"));
45
+ var fs = __toESM(require("fs"));
46
+ var os = __toESM(require("os"));
39
47
  var path = __toESM(require("path"));
48
+ var START_TIME_GRACE_MS = 2e3;
49
+ var DEFAULT_REGISTRY_FILE = path.join(process.cwd(), ".data", "pty-pids.json");
50
+ function readPtyRegistry(file = DEFAULT_REGISTRY_FILE) {
51
+ try {
52
+ const raw = fs.readFileSync(file, "utf8");
53
+ const parsed = JSON.parse(raw);
54
+ if (!Array.isArray(parsed)) return [];
55
+ return parsed.filter(
56
+ (r) => !!r && typeof r.pid === "number" && Number.isFinite(r.pid)
57
+ );
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+ function writePtyRegistry(records, file = DEFAULT_REGISTRY_FILE) {
63
+ try {
64
+ fs.mkdirSync(path.dirname(file), { recursive: true });
65
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
66
+ fs.writeFileSync(tmp, JSON.stringify(records, null, 2));
67
+ fs.renameSync(tmp, file);
68
+ } catch {
69
+ }
70
+ }
71
+ function isPidAlive(pid) {
72
+ try {
73
+ process.kill(pid, 0);
74
+ return true;
75
+ } catch (err) {
76
+ return err?.code === "EPERM";
77
+ }
78
+ }
79
+ function processStartTimeMs(pid) {
80
+ try {
81
+ if (os.platform() === "win32") {
82
+ const cmd = `powershell -NoProfile -NonInteractive -Command "$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($p) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() }"`;
83
+ const out2 = (0, import_child_process.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
84
+ const ms2 = parseInt(out2, 10);
85
+ return Number.isFinite(ms2) ? ms2 : null;
86
+ }
87
+ const out = (0, import_child_process.execSync)(`ps -o lstart= -p ${pid}`, {
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "pipe", "ignore"]
90
+ }).trim();
91
+ if (!out) return null;
92
+ const ms = Date.parse(out);
93
+ return Number.isNaN(ms) ? null : ms;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ function processIdentityMatches(record, startTimeOf) {
99
+ const startMs = startTimeOf(record.pid);
100
+ if (startMs == null) return false;
101
+ return startMs <= record.startedAt + START_TIME_GRACE_MS;
102
+ }
103
+ function killTree(pid) {
104
+ try {
105
+ if (os.platform() === "win32") {
106
+ (0, import_child_process.execSync)(`taskkill /T /F /PID ${pid}`, { stdio: "ignore" });
107
+ } else {
108
+ try {
109
+ process.kill(-pid, "SIGKILL");
110
+ } catch {
111
+ process.kill(pid, "SIGKILL");
112
+ }
113
+ }
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+ function reapRegisteredPtys(options = {}) {
120
+ const file = options.file ?? DEFAULT_REGISTRY_FILE;
121
+ const isAlive = options.isAlive ?? isPidAlive;
122
+ const kill = options.kill ?? killTree;
123
+ const startTimeOf = options.startTimeOf ?? processStartTimeMs;
124
+ const protectedPids = /* @__PURE__ */ new Set([process.pid, ...options.protectedPids ?? []]);
125
+ const records = readPtyRegistry(file);
126
+ const reaped = [];
127
+ const skipped = [];
128
+ const failed = [];
129
+ const survivors = [];
130
+ for (const record of records) {
131
+ const { pid } = record;
132
+ if (protectedPids.has(pid) || !isAlive(pid)) {
133
+ skipped.push(pid);
134
+ continue;
135
+ }
136
+ if (!processIdentityMatches(record, startTimeOf)) {
137
+ skipped.push(pid);
138
+ continue;
139
+ }
140
+ if (kill(pid)) {
141
+ reaped.push(pid);
142
+ } else {
143
+ failed.push(pid);
144
+ survivors.push(record);
145
+ }
146
+ }
147
+ writePtyRegistry(survivors, file);
148
+ return { reaped, skipped, failed };
149
+ }
150
+
151
+ // electron/terminal/ipc-relay.ts
40
152
  var TerminalRelay = class _TerminalRelay {
41
153
  constructor(getWindow) {
154
+ /**
155
+ * Event bus for main-process consumers (e.g. the Teams service) that need
156
+ * server→main copilot/terminal events without going through the renderer.
157
+ * Emits: 'copilot-event' (agentId, event), 'copilot-turn-start' (agentId),
158
+ * 'copilot-turn-end' (agentId), 'copilot-tool-start' (agentId, toolName, toolId, status),
159
+ * 'session-meta-updated' (agentId, meta), 'terminal-exit' (agentId, exitCode).
160
+ */
161
+ this.mainEvents = new import_events.EventEmitter();
42
162
  this.server = null;
43
163
  this.pendingRequests = /* @__PURE__ */ new Map();
44
164
  /** True while a deliberate shutdown→respawn is in progress (prevents double-spawn). */
@@ -54,9 +174,9 @@ var TerminalRelay = class _TerminalRelay {
54
174
  // ── Server Lifecycle ──────────────────────────────────────────
55
175
  spawnServer(distDir) {
56
176
  return new Promise((resolve, reject) => {
57
- const serverPath = path.join(distDir, "terminal", "server.js");
177
+ const serverPath = path2.join(distDir, "terminal", "server.js");
58
178
  console.log("[Relay] Forking terminal server:", serverPath);
59
- this.server = (0, import_child_process.fork)(serverPath, [], {
179
+ this.server = (0, import_child_process2.fork)(serverPath, [], {
60
180
  cwd: process.cwd(),
61
181
  stdio: ["pipe", "inherit", "inherit", "ipc"]
62
182
  });
@@ -80,6 +200,14 @@ var TerminalRelay = class _TerminalRelay {
80
200
  }
81
201
  const win = this.getWindow();
82
202
  if (win && !win.isDestroyed()) {
203
+ try {
204
+ const { reaped } = reapRegisteredPtys();
205
+ if (reaped.length > 0) {
206
+ console.log(`[Relay] Reaped ${reaped.length} orphaned PTY tree(s) from crashed server:`, reaped);
207
+ }
208
+ } catch (e) {
209
+ console.error("[Relay] Failed to reap orphaned PTYs after crash:", e);
210
+ }
83
211
  console.log("[Relay] Respawning terminal server...");
84
212
  this.spawnServer(distDir).catch(
85
213
  (e) => console.error("[Relay] Failed to respawn:", e)
@@ -124,7 +252,7 @@ var TerminalRelay = class _TerminalRelay {
124
252
  process.kill(oldPid, 0);
125
253
  console.log(`[Relay] Server PID ${oldPid} still alive after timeout \u2014 force killing`);
126
254
  if (process.platform === "win32") {
127
- (0, import_child_process.execSync)(`taskkill /T /F /PID ${oldPid}`, { stdio: "ignore" });
255
+ (0, import_child_process2.execSync)(`taskkill /T /F /PID ${oldPid}`, { stdio: "ignore" });
128
256
  } else {
129
257
  process.kill(oldPid, "SIGKILL");
130
258
  }
@@ -163,7 +291,33 @@ var TerminalRelay = class _TerminalRelay {
163
291
  });
164
292
  }
165
293
  id() {
166
- return crypto.randomUUID();
294
+ return crypto2.randomUUID();
295
+ }
296
+ // ── Main-process gateway (for the Teams service) ──────────────
297
+ // These let a main-process consumer talk to the terminal server directly,
298
+ // reusing the same request/response plumbing as the renderer IPC handlers.
299
+ mainGetSessionId(officeId, agentId) {
300
+ return this.request({ type: "get-session-id", requestId: this.id(), officeId, agentId });
301
+ }
302
+ /** True only when the agent's PTY is alive AND the CLI has signalled ready. */
303
+ mainIsAgentReady(officeId, agentId) {
304
+ return this.request({ type: "query-agent-statuses", requestId: this.id(), officeId }).then((statuses) => {
305
+ const s = statuses?.[agentId];
306
+ return !!(s && s.alive && s.ready);
307
+ });
308
+ }
309
+ mainGetSessionMeta(officeId, agentId) {
310
+ return this.request({ type: "get-session-meta", requestId: this.id(), officeId, agentId });
311
+ }
312
+ mainWrite(officeId, agentId, data) {
313
+ return this.request({ type: "write", requestId: this.id(), officeId, agentId, data });
314
+ }
315
+ mainSubmitPrompt(officeId, agentId, prompt, label) {
316
+ return this.request({ type: "submit-prompt", requestId: this.id(), officeId, agentId, prompt, label });
317
+ }
318
+ /** Fire-and-forget: control whether copilot-events are mirrored to main for an agent without a viewer. */
319
+ mainSetAgentForwarding(officeId, agentId, enabled) {
320
+ this.send({ type: "set-agent-forwarding", officeId, agentId, enabled });
167
321
  }
168
322
  handleServerMessage(msg, readyTimeout, onReady) {
169
323
  if (msg.type === "ready") {
@@ -189,6 +343,29 @@ var TerminalRelay = class _TerminalRelay {
189
343
  }
190
344
  return;
191
345
  }
346
+ switch (msg.type) {
347
+ case "copilot-event":
348
+ this.mainEvents.emit("copilot-event", msg.agentId, msg.event);
349
+ break;
350
+ case "copilot-turn-start":
351
+ this.mainEvents.emit("copilot-turn-start", msg.agentId);
352
+ break;
353
+ case "copilot-turn-end":
354
+ this.mainEvents.emit("copilot-turn-end", msg.agentId);
355
+ break;
356
+ case "copilot-user-message":
357
+ this.mainEvents.emit("copilot-user-message", msg.agentId, msg.text);
358
+ break;
359
+ case "copilot-tool-start":
360
+ this.mainEvents.emit("copilot-tool-start", msg.agentId, msg.toolName, msg.toolId, msg.status);
361
+ break;
362
+ case "session-meta-updated":
363
+ this.mainEvents.emit("session-meta-updated", msg.agentId, msg.meta);
364
+ break;
365
+ case "terminal-exit":
366
+ this.mainEvents.emit("terminal-exit", msg.agentId, msg.exitCode);
367
+ break;
368
+ }
192
369
  const win = this.getWindow();
193
370
  if (!win || win.isDestroyed()) return;
194
371
  switch (msg.type) {
@@ -199,7 +376,7 @@ var TerminalRelay = class _TerminalRelay {
199
376
  win.webContents.send("terminal-exit", msg.agentId, msg.exitCode);
200
377
  break;
201
378
  case "copilot-event":
202
- win.webContents.send("copilot-event", msg.agentId, msg.event);
379
+ if (!msg.mainOnly) win.webContents.send("copilot-event", msg.agentId, msg.event);
203
380
  break;
204
381
  case "copilot-tool-start":
205
382
  win.webContents.send("copilot-tool-start", msg.agentId, msg.toolName, msg.toolId, msg.status);
@@ -214,7 +391,7 @@ var TerminalRelay = class _TerminalRelay {
214
391
  win.webContents.send("copilot-turn-start", msg.agentId);
215
392
  break;
216
393
  case "copilot-user-message":
217
- win.webContents.send("copilot-user-message", msg.agentId);
394
+ win.webContents.send("copilot-user-message", msg.agentId, msg.text);
218
395
  break;
219
396
  case "session-meta-updated":
220
397
  win.webContents.send("session-meta-updated", msg.agentId, msg.meta);
@@ -246,6 +423,14 @@ var TerminalRelay = class _TerminalRelay {
246
423
  this.send({ type: "resize", officeId, agentId, cols, rows });
247
424
  return { success: true };
248
425
  });
426
+ import_electron.ipcMain.handle("set-yolo", (_event, enabled) => {
427
+ this.send({ type: "set-yolo", enabled: !!enabled });
428
+ return { success: true };
429
+ });
430
+ import_electron.ipcMain.handle("set-additional-params", (_event, params) => {
431
+ this.send({ type: "set-additional-params", params: String(params ?? "") });
432
+ return { success: true };
433
+ });
249
434
  import_electron.ipcMain.handle(
250
435
  "terminal-kill",
251
436
  (_event, officeId, agentId) => this.request({ type: "kill", requestId: this.id(), officeId, agentId })
@@ -32,6 +32,14 @@ import_electron.contextBridge.exposeInMainWorld("copilotBridge", {
32
32
  terminalKill: (officeId, agentId) => {
33
33
  return import_electron.ipcRenderer.invoke("terminal-kill", officeId, agentId);
34
34
  },
35
+ // YOLO mode: push global flag to the PTY server so new copilot sessions launch with --yolo.
36
+ setYolo: (enabled) => {
37
+ return import_electron.ipcRenderer.invoke("set-yolo", enabled);
38
+ },
39
+ // Additional parameters: push the effective param string (empty = none) to the PTY server.
40
+ setAdditionalParams: (params) => {
41
+ return import_electron.ipcRenderer.invoke("set-additional-params", params);
42
+ },
35
43
  terminalExists: (officeId, agentId) => {
36
44
  return import_electron.ipcRenderer.invoke("terminal-exists", officeId, agentId);
37
45
  },
@@ -160,5 +168,30 @@ import_electron.contextBridge.exposeInMainWorld("copilotBridge", {
160
168
  // terminalWrite to implement Paste in the terminal context menu.
161
169
  clipboardReadText: () => {
162
170
  return import_electron.ipcRenderer.invoke("clipboard-read-text");
171
+ },
172
+ // ── Teams Remote Agents (011) ────────────────────────────────
173
+ teamsStatus: (args) => {
174
+ return import_electron.ipcRenderer.invoke("teams:status", args ?? {});
175
+ },
176
+ teamsRegister: (ctx) => {
177
+ return import_electron.ipcRenderer.invoke("teams:register", ctx);
178
+ },
179
+ teamsStop: (args) => {
180
+ return import_electron.ipcRenderer.invoke("teams:stop", args);
181
+ },
182
+ teamsReconcile: () => {
183
+ return import_electron.ipcRenderer.invoke("teams:reconcile");
184
+ },
185
+ teamsGetSettings: () => {
186
+ return import_electron.ipcRenderer.invoke("teams:getSettings");
187
+ },
188
+ teamsSaveSettings: (settings) => {
189
+ return import_electron.ipcRenderer.invoke("teams:saveSettings", { settings });
190
+ },
191
+ onTeamsStatusChanged: (callback) => {
192
+ import_electron.ipcRenderer.on("teams:status:changed", (_event, status) => callback(status));
193
+ },
194
+ onTeamsToast: (callback) => {
195
+ import_electron.ipcRenderer.on("teams:toast", (_event, toast) => callback(toast));
163
196
  }
164
197
  });