copilotoffice 2.1.0 → 2.2.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/README.md CHANGED
@@ -1,44 +1,58 @@
1
- # Agency Office 🏢
1
+ # Copilot Office 🏢
2
2
 
3
- A 2D pixel-art RPG-style game where you walk around a virtual office and interact with AI agents. Each NPC runs a real Copilot CLI session with full coding capabilities — plan tasks, debug code, and orchestrate multi-agent workflows from inside a game.
3
+ A 2D pixel-art RPG-style desktop game where you walk around a virtual office and interact with AI agents. Each NPC runs a **real GitHub Copilot CLI session** with full coding capabilities — plan tasks, debug code, and orchestrate multi-agent workflows from inside a game. You can even bring an agent **online in a Microsoft Teams channel** and drive it from your phone.
4
4
 
5
- ![Agency Office](https://raw.githubusercontent.com/dan1510123/CopilotOffice/main/assets/game-screenshot.png)
5
+ ![Copilot Office](https://raw.githubusercontent.com/dan1510123/CopilotOffice/main/assets/game-screenshot.png)
6
6
 
7
7
  ## Features
8
8
 
9
- - **Pixel-art office environment** — all sprites procedurally generated in code, no external image assets
10
- - **3 active NPC agents by default**, each with specialized capabilities:
9
+ - **Pixel-art office environment** — every sprite is procedurally generated in code; there are no external image assets
10
+ - **Real Copilot agents** — each NPC runs an actual Copilot CLI session, rendered live in an xterm.js terminal
11
+ - **3 active NPC agents by default**, each with a distinct personality:
11
12
  - **Gene** (Generalist) — general-purpose coding, debugging, and research
12
- - **Dan** (Debugger) — bug investigation and root cause analysis
13
- - **Alice** (Admin) — has direct access to edit this game's UI code (`workingDir: '.'`)
14
- - **6 reserve agent slots** — Azure, Val, Rex, Doc, Scout, and Penny have pre-generated sprites ready to activate
15
- - **Arthur (Architect)** appears in fleet v-team offices (and can be toggled into the default office in config)
16
- - **Real terminal integration** via xterm.js agents run actual Copilot CLI sessions through node-pty
17
- - **Multi-office management** — switch between projects with independent agent state per office
18
- - **Meeting Mode** — private meeting room for planning and decomposing complex tasks into structured subtasks
19
- - **Fleet execution** — parallel agent spawning in dedicated v-team offices for approved task plans
20
- - **Real-time status badges** — agent states (thinking, waiting, ready, slacking) with animated indicators
13
+ - **Dan** (Debugger) — bug investigation and root-cause analysis
14
+ - **Alice** (Admin) — has direct access to edit this game's own source code (`workingDir: '.'`)
15
+ - **6 reserve agents** — Azure (Cloud Wizard), Val (Validator), Rex (Deployer), Doc (Code Doctor), Scout (Ranger), and Penny (Accountant) have pre-generated sprites ready to seat at an empty desk
16
+ - **Arthur (the Architect)** — hosts Meeting Mode and appears in fleet v-team offices (can be toggled into the default office in config)
17
+ - **Teams remote agents** bring any agent online in a Microsoft Teams channel thread; anyone can reply in-thread to drive the agent's terminal session and get answers posted back (feature-flagged)
18
+ - **Multi-office management** — switch between projects with independent agent state and working directories per office
19
+ - **Meeting Mode** — a private meeting room where Arthur decomposes a complex request into a structured, reviewable plan
20
+ - **Fleet execution** — approved plans spin up parallel agent sessions in a dedicated v-team office
21
+ - **Real-time status badges** — agent states (slacking starting → ready ↔ waiting/thinking) with animated indicators
21
22
  - **Toast & OS notifications** — configurable per-event notifications for agent activity
22
- - **Session persistence** — terminal sessions and history survive restarts
23
- - **Player customization** — customizable character colors
24
- - **Mini-games** — Pong and Basketball (behind feature flags) for breaks
23
+ - **Session persistence** — offices, seated agents, and terminal sessions survive restarts
24
+ - **Player & sprite customization** — customize your character's appearance and colors
25
+ - **Mini-games** — a built-in Galaxian arcade game (Pong and Basketball are also included behind feature flags)
25
26
  - **Hot reload** development mode with file watching
26
27
 
27
28
  ## Tech Stack
28
29
 
29
- - **Phaser 3** — 2D game framework (sole renderer)
30
- - **Electron 40+** — desktop app with Node.js integration
30
+ - **Phaser 3** — 2D game framework (the sole renderer)
31
+ - **Electron 40+** — desktop shell with a Node.js main process
31
32
  - **TypeScript** — strict mode throughout
32
- - **esbuild** — fast bundling for game and Electron code
33
+ - **esbuild** — fast bundling for both the game and Electron code
33
34
  - **xterm.js** — terminal emulator for agent conversations
34
- - **node-pty** — pseudo-terminal for running CLI processes
35
+ - **node-pty** — pseudo-terminal that hosts the Copilot CLI
36
+ - **@github/copilot-sdk** — SDK control plane for the `ui-server` terminal backend
37
+ - **ws** — WebSocket transport (SDK runtime + Teams real-time receive)
38
+
39
+ ### Terminal backends
40
+
41
+ The terminal server (`electron/terminal/server.ts`) selects a backend via the `COPILOT_TERMINAL_BACKEND` environment variable:
42
+
43
+ - **`node-pty`** (fallback, always available) — spawns the real Copilot TUI directly, one PTY per agent
44
+ - **`ui-server`** (default) — node-pty hosts one `copilot --ui-server` runtime per office and the Copilot SDK attaches over a local port; automatically falls back to `node-pty` when the CLI can't host `--ui-server`
45
+ - **`sdk`** (legacy) — the SDK spawns its own headless runtime over stdio
35
46
 
36
47
  ## Getting Started
37
48
 
38
49
  ### Prerequisites
39
50
 
40
- - Node.js 18+
41
- - npm
51
+ To use the app **in full** you'll need:
52
+
53
+ - **Node.js 18+** and **npm**
54
+ - **GitHub Copilot access** — the agents run the real Copilot CLI, so you must be signed in to a GitHub account with an active Copilot subscription. The CLI runtime ships with the app via the `@github/copilot-sdk` platform package; on first run, authenticate through the CLI as prompted.
55
+ - **(Teams remote agents only) Azure CLI** — the Teams feature acquires Microsoft Graph and IC3 tokens via `az account get-access-token`, so you must have the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and be logged in (`az login`) with an account that has access to the target Teams channel. This feature is off by default and enabled in Settings.
42
56
 
43
57
  ### Install from npm (global command)
44
58
 
@@ -50,7 +64,8 @@ copilotoffice
50
64
  ### Install from source (development)
51
65
 
52
66
  ```bash
53
- cd AgencyOffice
67
+ git clone https://github.com/dan1510123/CopilotOffice.git
68
+ cd CopilotOffice
54
69
  npm install
55
70
 
56
71
  # Build and run
@@ -69,21 +84,40 @@ npm run dev
69
84
  | `E` | Interact with nearby agent or object |
70
85
  | `F10` | Close terminal |
71
86
  | `Escape` | Close terminal or mini-game |
72
- | `Ctrl+Shift+N` | New terminal session |
87
+ | `Ctrl+Shift+N` | New terminal session (terminal focused) |
73
88
 
74
89
  ## Project Structure
75
90
 
76
91
  ```
77
- AgencyOffice/
92
+ CopilotOffice/
78
93
  ├── electron/ # Electron main process
79
94
  │ ├── main.ts # Window, IPC handlers, hot reload
80
- │ ├── cli-bridge.ts # Mock/placeholder (not used at runtime)
81
- └── terminal/ # Terminal server subsystem
82
- ├── server.ts # PTY owner (forked child process)
83
- ├── ipc-relay.ts # IPC bridge (renderer ↔ main ↔ server)
84
- ├── preload.ts # Context bridge (window.copilotBridge)
85
- ├── protocol.ts # IPC message type definitions
86
- └── events-watcher.ts # Copilot CLI event file parser
95
+ │ ├── nonTerminalIpc.ts # Non-terminal IPC handlers
96
+ ├── officeFileStore.ts # Office persistence on disk
97
+ ├── cli-bridge.ts # Legacy placeholder (not used at runtime)
98
+ ├── terminal/ # Terminal server subsystem
99
+ ├── server.ts # PTY/SDK owner (forked child process)
100
+ ├── terminal-backend.ts # Backend selection (node-pty / ui-server / sdk)
101
+ │ ├── pty-registry.ts # Live PTY/session bookkeeping
102
+ │ │ ├── agent-viewers.ts # Active-viewer dual-key invariant helpers
103
+ │ │ ├── office-foreground.ts # Foreground session selection (ui-server)
104
+ │ │ ├── session-repair.ts # Session recovery
105
+ │ │ ├── ipc-relay.ts # IPC bridge (renderer ↔ main ↔ server)
106
+ │ │ ├── preload.ts # Context bridge (window.copilotBridge)
107
+ │ │ ├── protocol.ts # IPC message type definitions
108
+ │ │ ├── event-source.ts # Backend-agnostic event source
109
+ │ │ └── events-watcher.ts # Copilot CLI event file parser
110
+ │ └── teams/ # Teams remote agents (main-process service)
111
+ │ ├── teamsService.ts # Orchestrator (register/route/reply lifecycle)
112
+ │ ├── auth.ts # Graph + IC3 tokens via `az`
113
+ │ ├── graphClient.ts # Send channel messages
114
+ │ ├── trouterClient.ts # Real-time receive (WebSocket)
115
+ │ ├── chatsvcClient.ts # Poll fallback receive
116
+ │ ├── messageFilter.ts # Dedup / marker / classify pipeline
117
+ │ ├── dispatchQueue.ts # Per-agent FIFO dispatch
118
+ │ ├── sessionGateway.ts # Adapter over the terminal server
119
+ │ ├── onlineAgentsStore.ts # Online-agent persistence + GC
120
+ │ └── ... # channelLink, marker, chunk, resolvers, IPC
87
121
  ├── src/ # Renderer process (Phaser + DOM)
88
122
  │ ├── main.ts # Entry point — DOM layout, Phaser init, IPC wiring
89
123
  │ ├── index.html # HTML host page
@@ -91,44 +125,31 @@ AgencyOffice/
91
125
  │ │ ├── BootScene.ts # Procedural sprite generation
92
126
  │ │ ├── OfficeScene.ts # Main game scene (layout, NPCs, interactions)
93
127
  │ │ └── MeetingScene.ts # Meeting room with Arthur for planning
94
- │ ├── entities/ # Game entities
95
- ├── Player.ts # Player character with movement
96
- │ └── NPC.ts # NPC agents with status badges
97
- │ ├── sprites/ # Procedural sprite generation
98
- │ │ ├── SpriteGenerator.ts # Sprite sheet generation
99
- │ │ └── DirectionalSprite.ts # 4-direction animation utilities
100
- │ ├── ui/ # UI overlays
128
+ │ ├── entities/ # Game entities (Player, NPC)
129
+ │ ├── sprites/ # Procedural sprite generation + animation
130
+ ├── ui/ # DOM overlays
101
131
  │ │ ├── TerminalOverlay.ts # xterm.js terminal for agent sessions
132
+ │ │ ├── SeriousTerminalController.ts # Split-pane terminal controller
102
133
  │ │ ├── FleetDashboard.ts # Fleet execution dashboard
103
- │ │ ├── PongGame.ts # Pong mini-game
104
- │ │ ├── BasketballGame.ts # Basketball mini-game
105
- │ │ ├── ToastNotification.ts # Toast notification popups
106
- │ │ ├── NotificationService.ts
107
- │ │ ├── NotificationSettingsPanel.ts
108
- │ │ ├── CameraDragController.ts
109
- │ │ └── DialogBox.ts # Legacy (deprecated)
110
- │ ├── input/ # Keyboard focus management
111
- ├── InputManager.ts # Central coordinator
112
- │ │ ├── GameInputListener.ts
113
- │ │ ├── GlobalInputListener.ts
114
- │ │ └── TerminalInputListener.ts
115
- │ ├── office/ # Multi-office state management
116
- │ │ └── officeManager.ts # Office CRUD, agent status tracking
134
+ │ │ ├── SettingsPanel.ts # Settings overlay
135
+ │ │ ├── TeamsSettingsOverlay.ts # Teams feature settings
136
+ │ │ ├── SpriteCustomizerPanel.ts # Player appearance customization
137
+ │ │ ├── GalaxianGame.ts # Galaxian mini-game
138
+ │ │ ├── PongGame.ts / BasketballGame.ts # Mini-games (feature-flagged)
139
+ │ │ ├── NotificationService.ts / NotificationSettingsPanel.ts / ToastNotification.ts
140
+ │ │ └── CameraDragController.ts / DialogBox.ts
141
+ │ ├── input/ # Keyboard focus management (InputManager + listeners)
142
+ │ ├── office/ # Multi-office state management (officeManager.ts)
117
143
  │ ├── meeting/ # Meeting mode & fleet orchestration
118
- │ │ ├── types.ts # MeetingPlan, TaskAssignment, FleetStatus
119
- │ │ ├── planParser.ts # Terminal output structured plan
120
- │ │ ├── planApproval.ts # Plan review overlay
121
- │ │ ├── fleetOrchestrator.ts # Parallel agent spawning
122
- │ │ ├── fleetTracker.ts # Fleet state machine
123
- │ │ └── fleetVisualizer.ts # Fleet NPC visualization
144
+ │ │ ├── types.ts / planParser.ts / planApproval.ts
145
+ │ │ └── fleetOrchestrator.ts / fleetTracker.ts / fleetVisualizer.ts
124
146
  │ ├── layouts/ # Layout system
125
- │ │ ├── types.ts # DashboardRenderer, LayoutDefinition
126
- │ │ ├── index.ts # Layout registry
147
+ │ │ ├── types.ts / index.ts # Layout registry + behaviors
127
148
  │ │ ├── default/ # Default office layout
128
149
  │ │ └── fleet/ # Fleet v-team layout
129
150
  │ └── config/ # Static configuration
130
- │ ├── agents.ts # Agent definitions & fleet config
131
- │ ├── depths.ts # Phaser depth layer constants
151
+ │ ├── agents.ts # Agent definitions, reserve + fleet config
152
+ │ ├── depths.ts / zIndex.ts # Phaser depth + DOM z-index constants
132
153
  │ ├── notifications.ts # Notification event settings
133
154
  │ ├── meetingPrompt.ts # Meeting coordinator prompt
134
155
  │ └── playerCustomization.ts # Player color customization
@@ -137,23 +158,27 @@ AgencyOffice/
137
158
 
138
159
  ## Adding New Agents
139
160
 
140
- Edit `src/config/agents.ts` to add new NPCs. Six reserve agent slots (Azure, Val, Rex, Doc, Scout, Penny) already have pre-generated sprites — activate one by adding its config to the `AGENTS` array:
161
+ Edit `src/config/agents.ts` to add new NPCs. Six reserve agent slots (Azure, Val, Rex, Doc, Scout, Penny) already have pre-generated sprites — activate one by adding its config, or add a brand-new entry to the `AGENTS` array:
141
162
 
142
163
  ```typescript
143
164
  {
144
165
  id: 'unique-id',
145
166
  name: 'Display Name',
146
- skill: 'copilot-skill-name',
167
+ skill: 'general',
147
168
  sprite: 'sprite_key',
148
- color: 0xff0000, // Hex color for procedural sprite
149
- position: { x: 5, y: 7 }, // Grid position in office
150
- greeting: "Hello message when player approaches",
169
+ color: 0xff0000, // Hex color for the procedural sprite
170
+ position: { x: 5, y: 7 }, // Grid position in the office (20×12 tile grid)
171
+ greeting: "Hello message shown when the player approaches",
151
172
  description: 'Short description',
152
- workingDir: 'optional/path', // Optional custom working directory
173
+ workingDir: 'optional/path', // Optional custom working directory
153
174
  }
154
175
  ```
155
176
 
156
- Sprites are auto-generated based on the color — no image assets needed.
177
+ Sprites are auto-generated from the color — no image assets needed.
178
+
179
+ > **Tip:** Don't hardcode agent IDs in scene/layout/dashboard logic. Use the named
180
+ > constants exported from `src/config/agents.ts` (`GENERALIST_AGENT_ID`,
181
+ > `DEBUGGER_AGENT_ID`, `ADMIN_AGENT_ID`, `ARCHITECT_AGENT_ID`, `DEFAULT_PLAN_AGENT_IDS`).
157
182
 
158
183
  ## Development
159
184
 
@@ -168,13 +193,21 @@ npm run build
168
193
  npm run electron
169
194
  ```
170
195
 
196
+ ## Testing
197
+
198
+ ```bash
199
+ npm run test # Vitest unit/integration suite
200
+ npm run test:coverage # Vitest with coverage output
201
+ npm run test:e2e # Playwright end-to-end tests (runs a build first)
202
+ ```
203
+
171
204
  ## Release channels
172
205
 
173
- - **Stable**: `npm i -g copilotoffice` (uses npm `latest` dist-tag)
174
- - **Beta**: `npm i -g copilotoffice@beta` (uses npm `beta` dist-tag)
206
+ - **Stable**: `npm i -g copilotoffice` (uses the npm `latest` dist-tag)
207
+ - **Beta**: `npm i -g copilotoffice@beta` (uses the npm `beta` dist-tag)
175
208
 
176
- For maintainers: pushing to GitHub is not enough for `npm i -g copilotoffice` by name.
177
- You must publish to npm. Typical flow:
209
+ For maintainers: pushing to GitHub is not enough for `npm i -g copilotoffice` by name
210
+ you must publish to npm. Typical flow:
178
211
 
179
212
  ```bash
180
213
  npm run build
@@ -161,6 +161,8 @@ var TerminalRelay = class _TerminalRelay {
161
161
  this.shuttingDown = false;
162
162
  /** Requests that arrived while the server was not connected. Flushed on ready. */
163
163
  this.queuedRequests = [];
164
+ /** Latest backend-selection outcome reported by the server on 'ready'. */
165
+ this.backendInfo = null;
164
166
  this.getWindow = getWindow;
165
167
  }
166
168
  static {
@@ -320,6 +322,15 @@ var TerminalRelay = class _TerminalRelay {
320
322
  clearTimeout(readyTimeout);
321
323
  this.shuttingDown = false;
322
324
  console.log("[Relay] Terminal server ready");
325
+ if (msg.backend) {
326
+ this.backendInfo = msg.backend;
327
+ if (msg.backend.fellBack) {
328
+ const win2 = this.getWindow();
329
+ if (win2 && !win2.isDestroyed()) {
330
+ win2.webContents.send("backend-fallback", this.backendInfo);
331
+ }
332
+ }
333
+ }
323
334
  if (this.queuedRequests.length > 0) {
324
335
  console.log(`[Relay] Flushing ${this.queuedRequests.length} queued request(s)`);
325
336
  for (const queued of this.queuedRequests) {
@@ -395,17 +406,24 @@ var TerminalRelay = class _TerminalRelay {
395
406
  case "terminal-preload-status":
396
407
  win.webContents.send("terminal-preload-status", msg.agentId, msg.status);
397
408
  break;
409
+ case "backend-online":
410
+ win.webContents.send("backend-online", msg.officeId, msg.backend);
411
+ break;
412
+ case "backend-session-fallback":
413
+ win.webContents.send("backend-session-fallback", msg.officeId, msg.agentId, msg.reason);
414
+ break;
398
415
  }
399
416
  }
400
417
  // ── IPC Handler Registration ──────────────────────────────────
401
418
  registerIpc() {
419
+ import_electron.ipcMain.handle("terminal-backend-info", () => this.backendInfo);
402
420
  import_electron.ipcMain.handle(
403
421
  "terminal-start",
404
422
  (_event, officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode) => this.request({ type: "start", requestId: this.id(), officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode })
405
423
  );
406
424
  import_electron.ipcMain.handle(
407
425
  "terminal-attach",
408
- (_event, officeId, agentId) => this.request({ type: "attach", requestId: this.id(), officeId, agentId })
426
+ (_event, officeId, agentId, foreground) => this.request({ type: "attach", requestId: this.id(), officeId, agentId, foreground })
409
427
  );
410
428
  import_electron.ipcMain.handle("terminal-detach", (_event, officeId, agentId) => {
411
429
  this.send({ type: "detach", officeId, agentId });
@@ -165,6 +165,8 @@ var TerminalRelay = class _TerminalRelay {
165
165
  this.shuttingDown = false;
166
166
  /** Requests that arrived while the server was not connected. Flushed on ready. */
167
167
  this.queuedRequests = [];
168
+ /** Latest backend-selection outcome reported by the server on 'ready'. */
169
+ this.backendInfo = null;
168
170
  this.getWindow = getWindow;
169
171
  }
170
172
  static {
@@ -324,6 +326,15 @@ var TerminalRelay = class _TerminalRelay {
324
326
  clearTimeout(readyTimeout);
325
327
  this.shuttingDown = false;
326
328
  console.log("[Relay] Terminal server ready");
329
+ if (msg.backend) {
330
+ this.backendInfo = msg.backend;
331
+ if (msg.backend.fellBack) {
332
+ const win2 = this.getWindow();
333
+ if (win2 && !win2.isDestroyed()) {
334
+ win2.webContents.send("backend-fallback", this.backendInfo);
335
+ }
336
+ }
337
+ }
327
338
  if (this.queuedRequests.length > 0) {
328
339
  console.log(`[Relay] Flushing ${this.queuedRequests.length} queued request(s)`);
329
340
  for (const queued of this.queuedRequests) {
@@ -399,17 +410,24 @@ var TerminalRelay = class _TerminalRelay {
399
410
  case "terminal-preload-status":
400
411
  win.webContents.send("terminal-preload-status", msg.agentId, msg.status);
401
412
  break;
413
+ case "backend-online":
414
+ win.webContents.send("backend-online", msg.officeId, msg.backend);
415
+ break;
416
+ case "backend-session-fallback":
417
+ win.webContents.send("backend-session-fallback", msg.officeId, msg.agentId, msg.reason);
418
+ break;
402
419
  }
403
420
  }
404
421
  // ── IPC Handler Registration ──────────────────────────────────
405
422
  registerIpc() {
423
+ import_electron.ipcMain.handle("terminal-backend-info", () => this.backendInfo);
406
424
  import_electron.ipcMain.handle(
407
425
  "terminal-start",
408
426
  (_event, officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode) => this.request({ type: "start", requestId: this.id(), officeId, agentId, workingDir, cols, rows, preseededPrompt, launchMode })
409
427
  );
410
428
  import_electron.ipcMain.handle(
411
429
  "terminal-attach",
412
- (_event, officeId, agentId) => this.request({ type: "attach", requestId: this.id(), officeId, agentId })
430
+ (_event, officeId, agentId, foreground) => this.request({ type: "attach", requestId: this.id(), officeId, agentId, foreground })
413
431
  );
414
432
  import_electron.ipcMain.handle("terminal-detach", (_event, officeId, agentId) => {
415
433
  this.send({ type: "detach", officeId, agentId });
@@ -43,8 +43,8 @@ import_electron.contextBridge.exposeInMainWorld("copilotBridge", {
43
43
  terminalExists: (officeId, agentId) => {
44
44
  return import_electron.ipcRenderer.invoke("terminal-exists", officeId, agentId);
45
45
  },
46
- terminalAttach: (officeId, agentId) => {
47
- return import_electron.ipcRenderer.invoke("terminal-attach", officeId, agentId);
46
+ terminalAttach: (officeId, agentId, foreground) => {
47
+ return import_electron.ipcRenderer.invoke("terminal-attach", officeId, agentId, foreground);
48
48
  },
49
49
  terminalDetach: (officeId, agentId) => {
50
50
  return import_electron.ipcRenderer.invoke("terminal-detach", officeId, agentId);
@@ -105,37 +105,61 @@ import_electron.contextBridge.exposeInMainWorld("copilotBridge", {
105
105
  loadOffices: () => {
106
106
  return import_electron.ipcRenderer.invoke("load-offices");
107
107
  },
108
- // Terminal event listeners
108
+ // Terminal event listeners. Each returns an unsubscribe function that removes
109
+ // ONLY this registration (not every listener on the channel), so a controller
110
+ // can dispose its own listener before re-registering. This prevents duplicate
111
+ // registrations from writing the same PTY byte to xterm more than once (the
112
+ // classic "double characters" bug). Callers may ignore the return value.
109
113
  onTerminalData: (callback) => {
110
- import_electron.ipcRenderer.on("terminal-data", (_event, agentId, data) => callback(agentId, data));
114
+ const handler = (_event, agentId, data) => callback(agentId, data);
115
+ import_electron.ipcRenderer.on("terminal-data", handler);
116
+ return () => import_electron.ipcRenderer.removeListener("terminal-data", handler);
111
117
  },
112
118
  onTerminalExit: (callback) => {
113
- import_electron.ipcRenderer.on("terminal-exit", (_event, agentId, exitCode) => callback(agentId, exitCode));
119
+ const handler = (_event, agentId, exitCode) => callback(agentId, exitCode);
120
+ import_electron.ipcRenderer.on("terminal-exit", handler);
121
+ return () => import_electron.ipcRenderer.removeListener("terminal-exit", handler);
114
122
  },
115
123
  onTerminalPreloadStatus: (callback) => {
116
- import_electron.ipcRenderer.on("terminal-preload-status", (_event, agentId, status) => callback(agentId, status));
124
+ const handler = (_event, agentId, status) => callback(agentId, status);
125
+ import_electron.ipcRenderer.on("terminal-preload-status", handler);
126
+ return () => import_electron.ipcRenderer.removeListener("terminal-preload-status", handler);
117
127
  },
118
128
  // Copilot activity event listeners
119
129
  onCopilotEvent: (callback) => {
120
- import_electron.ipcRenderer.on("copilot-event", (_event, agentId, copilotEvent) => callback(agentId, copilotEvent));
130
+ const handler = (_event, agentId, copilotEvent) => callback(agentId, copilotEvent);
131
+ import_electron.ipcRenderer.on("copilot-event", handler);
132
+ return () => import_electron.ipcRenderer.removeListener("copilot-event", handler);
121
133
  },
122
134
  onCopilotToolStart: (callback) => {
123
- import_electron.ipcRenderer.on("copilot-tool-start", (_event, agentId, toolName, toolId, status) => callback(agentId, toolName, toolId, status));
135
+ const handler = (_event, agentId, toolName, toolId, status) => callback(agentId, toolName, toolId, status);
136
+ import_electron.ipcRenderer.on("copilot-tool-start", handler);
137
+ return () => import_electron.ipcRenderer.removeListener("copilot-tool-start", handler);
124
138
  },
125
139
  onCopilotToolComplete: (callback) => {
126
- import_electron.ipcRenderer.on("copilot-tool-complete", (_event, agentId, toolId, success) => callback(agentId, toolId, success));
140
+ const handler = (_event, agentId, toolId, success) => callback(agentId, toolId, success);
141
+ import_electron.ipcRenderer.on("copilot-tool-complete", handler);
142
+ return () => import_electron.ipcRenderer.removeListener("copilot-tool-complete", handler);
127
143
  },
128
144
  onCopilotTurnEnd: (callback) => {
129
- import_electron.ipcRenderer.on("copilot-turn-end", (_event, agentId) => callback(agentId));
145
+ const handler = (_event, agentId) => callback(agentId);
146
+ import_electron.ipcRenderer.on("copilot-turn-end", handler);
147
+ return () => import_electron.ipcRenderer.removeListener("copilot-turn-end", handler);
130
148
  },
131
149
  onCopilotTurnStart: (callback) => {
132
- import_electron.ipcRenderer.on("copilot-turn-start", (_event, agentId) => callback(agentId));
150
+ const handler = (_event, agentId) => callback(agentId);
151
+ import_electron.ipcRenderer.on("copilot-turn-start", handler);
152
+ return () => import_electron.ipcRenderer.removeListener("copilot-turn-start", handler);
133
153
  },
134
154
  onCopilotUserMessage: (callback) => {
135
- import_electron.ipcRenderer.on("copilot-user-message", (_event, agentId) => callback(agentId));
155
+ const handler = (_event, agentId) => callback(agentId);
156
+ import_electron.ipcRenderer.on("copilot-user-message", handler);
157
+ return () => import_electron.ipcRenderer.removeListener("copilot-user-message", handler);
136
158
  },
137
159
  onSessionMetaUpdated: (callback) => {
138
- import_electron.ipcRenderer.on("session-meta-updated", (_event, agentId, meta) => callback(agentId, meta));
160
+ const handler = (_event, agentId, meta) => callback(agentId, meta);
161
+ import_electron.ipcRenderer.on("session-meta-updated", handler);
162
+ return () => import_electron.ipcRenderer.removeListener("session-meta-updated", handler);
139
163
  },
140
164
  removeTerminalListeners: () => {
141
165
  import_electron.ipcRenderer.removeAllListeners("terminal-data");
@@ -158,6 +182,19 @@ import_electron.contextBridge.exposeInMainWorld("copilotBridge", {
158
182
  showNativeNotification: (title, body) => {
159
183
  return import_electron.ipcRenderer.invoke("show-native-notification", title, body);
160
184
  },
185
+ // Terminal backend selection (ui-server / node-pty / sdk).
186
+ getBackendInfo: () => {
187
+ return import_electron.ipcRenderer.invoke("terminal-backend-info");
188
+ },
189
+ onBackendFallback: (callback) => {
190
+ import_electron.ipcRenderer.on("backend-fallback", (_event, info) => callback(info));
191
+ },
192
+ onBackendOnline: (callback) => {
193
+ import_electron.ipcRenderer.on("backend-online", (_event, officeId, backend) => callback(officeId, backend));
194
+ },
195
+ onBackendSessionFallback: (callback) => {
196
+ import_electron.ipcRenderer.on("backend-session-fallback", (_event, officeId, agentId, reason) => callback(officeId, agentId, reason));
197
+ },
161
198
  // Spec 003 follow-up: write to OS clipboard via Electron main process.
162
199
  // Bypasses Permissions API + focus restrictions that make
163
200
  // navigator.clipboard.writeText unreliable in xterm-focused contexts.