loadtoagent 1.0.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.
Files changed (82) hide show
  1. package/README.ko.md +175 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +175 -0
  4. package/bin/loadtoagent.js +171 -0
  5. package/docs/ARCHITECTURE.md +30 -0
  6. package/docs/PROVIDER-CONTRACTS.md +58 -0
  7. package/docs/assets/loadtoagent-dashboard.png +0 -0
  8. package/docs/assets/loadtoagent-demo.gif +0 -0
  9. package/main.js +493 -0
  10. package/package.json +133 -0
  11. package/preload.js +75 -0
  12. package/renderer/app-agent-actions.js +285 -0
  13. package/renderer/app-bootstrap.js +95 -0
  14. package/renderer/app-dashboard.js +361 -0
  15. package/renderer/app-drawer-content.js +203 -0
  16. package/renderer/app-drawer-data.js +41 -0
  17. package/renderer/app-drawer.js +183 -0
  18. package/renderer/app-events-dialogs.js +169 -0
  19. package/renderer/app-events-filters.js +74 -0
  20. package/renderer/app-events-navigation.js +96 -0
  21. package/renderer/app-events-sessions.js +195 -0
  22. package/renderer/app-events.js +16 -0
  23. package/renderer/app-graph-layout.js +71 -0
  24. package/renderer/app-graph-model.js +107 -0
  25. package/renderer/app-graph-orchestration.js +76 -0
  26. package/renderer/app-graph-view.js +544 -0
  27. package/renderer/app-run-modal.js +221 -0
  28. package/renderer/app-session-render.js +243 -0
  29. package/renderer/app-tmux-render.js +247 -0
  30. package/renderer/app.js +608 -0
  31. package/renderer/fonts/geist-ext.woff2 +0 -0
  32. package/renderer/fonts/geist.woff2 +0 -0
  33. package/renderer/i18n-messages.js +355 -0
  34. package/renderer/i18n.js +122 -0
  35. package/renderer/index.html +386 -0
  36. package/renderer/shared.js +26 -0
  37. package/renderer/styles-agent-map.css +401 -0
  38. package/renderer/styles-cards.css +600 -0
  39. package/renderer/styles-collaboration.css +534 -0
  40. package/renderer/styles-components.css +1293 -0
  41. package/renderer/styles-onboarding.css +344 -0
  42. package/renderer/styles-overlays.css +284 -0
  43. package/renderer/styles-product.css +686 -0
  44. package/renderer/styles-responsive-product.css +689 -0
  45. package/renderer/styles-responsive-runtime.css +718 -0
  46. package/renderer/styles-responsive-shell.css +533 -0
  47. package/renderer/styles-responsive-workflows.css +778 -0
  48. package/renderer/styles-run-composer.css +695 -0
  49. package/renderer/styles-settings.css +606 -0
  50. package/renderer/styles-terminal.css +1241 -0
  51. package/renderer/styles-tmux.css +1162 -0
  52. package/renderer/styles-workflow-map.css +513 -0
  53. package/renderer/styles-workflows.css +1217 -0
  54. package/renderer/styles.css +486 -0
  55. package/renderer/terminal-agent.js +152 -0
  56. package/renderer/terminal-events.js +226 -0
  57. package/renderer/terminal-workbench.js +464 -0
  58. package/renderer/terminal.js +497 -0
  59. package/src/agentMonitor/claudeParser.js +197 -0
  60. package/src/agentMonitor/codexCollaboration.js +95 -0
  61. package/src/agentMonitor/codexParser.js +447 -0
  62. package/src/agentMonitor/genericParser.js +244 -0
  63. package/src/agentMonitor/hierarchy.js +248 -0
  64. package/src/agentMonitor/sessionFiles.js +86 -0
  65. package/src/agentMonitor.js +591 -0
  66. package/src/agentRunner.js +465 -0
  67. package/src/bridgeServer.js +246 -0
  68. package/src/contracts.js +71 -0
  69. package/src/diagnostics.js +23 -0
  70. package/src/ipc/registerAgentIpc.js +12 -0
  71. package/src/ipc/registerAppIpc.js +20 -0
  72. package/src/ipc/registerTerminalIpc.js +50 -0
  73. package/src/ipc/registerTmuxIpc.js +30 -0
  74. package/src/ipc/registerWorkspaceIpc.js +14 -0
  75. package/src/monitorWorker.js +273 -0
  76. package/src/processMonitor.js +394 -0
  77. package/src/providerRegistry.js +120 -0
  78. package/src/terminalManager.js +438 -0
  79. package/src/tmuxController.js +183 -0
  80. package/src/tmuxMonitor.js +432 -0
  81. package/src/updateManager.js +339 -0
  82. package/src/workspaceStore.js +77 -0
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const net = require('net');
7
+ const crypto = require('crypto');
8
+ const { AGENT_PROVIDERS } = require('./terminalManager');
9
+ const { runBestEffort } = require('./diagnostics');
10
+
11
+ const PROTOCOL_VERSION = 1;
12
+ const MAX_FRAME_CHARS = 1024 * 1024;
13
+ const AUTH_TIMEOUT_MS = 10_000;
14
+
15
+ function bridgeDirectory(home = os.homedir()) {
16
+ return path.join(home, '.loadtoagent');
17
+ }
18
+
19
+ function discoveryFile(home = os.homedir()) {
20
+ return path.join(bridgeDirectory(home), 'bridge.json');
21
+ }
22
+
23
+ function endpointFor(platform = process.platform, home = os.homedir(), nonce = crypto.randomBytes(8).toString('hex')) {
24
+ const identity = crypto.createHash('sha256').update(`${home}:${nonce}`).digest('hex').slice(0, 18);
25
+ if (platform === 'win32') return `\\\\.\\pipe\\loadtoagent-${identity}`;
26
+ return path.join(os.tmpdir(), `loadtoagent-${typeof process.getuid === 'function' ? process.getuid() : 'user'}-${identity}.sock`);
27
+ }
28
+
29
+ function safeWriteJson(file, value) {
30
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
31
+ const temporary = `${file}.${process.pid}.tmp`;
32
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });
33
+ runBestEffort('bridge-temp-permissions', () => fs.chmodSync(temporary, 0o600));
34
+ fs.renameSync(temporary, file);
35
+ runBestEffort('bridge-discovery-permissions', () => fs.chmodSync(file, 0o600));
36
+ }
37
+
38
+ function sendFrame(socket, payload) {
39
+ if (!socket || socket.destroyed) return;
40
+ socket.write(`${JSON.stringify(payload)}\n`, 'utf8');
41
+ }
42
+
43
+ function validProvider(value) {
44
+ const provider = String(value || '').toLowerCase();
45
+ return AGENT_PROVIDERS[provider] ? provider : '';
46
+ }
47
+
48
+ function decodeBase64(value) {
49
+ const encoded = String(value || '');
50
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
51
+ throw new Error('브리지 입력 인코딩이 올바르지 않습니다.');
52
+ }
53
+ return Buffer.from(encoded, 'base64').toString('utf8');
54
+ }
55
+
56
+ class BridgeServer {
57
+ constructor(options = {}) {
58
+ this.terminalManager = options.terminalManager;
59
+ this.home = options.home || os.homedir();
60
+ this.platform = options.platform || process.platform;
61
+ this.endpoint = options.endpoint || endpointFor(this.platform, this.home);
62
+ this.token = options.token || crypto.randomBytes(32).toString('hex');
63
+ this.file = options.discoveryFile || discoveryFile(this.home);
64
+ this.server = null;
65
+ this.clients = new Map();
66
+ this.terminalListenersAttached = false;
67
+ this.onTerminalData = payload => this.forwardData(payload);
68
+ this.onTerminalState = payload => this.forwardState(payload);
69
+ }
70
+
71
+ start() {
72
+ if (!this.terminalManager) return Promise.reject(new Error('터미널 관리자가 준비되지 않았습니다.'));
73
+ if (this.server) return Promise.resolve(this.info());
74
+ if (this.platform !== 'win32') {
75
+ if (fs.existsSync(this.endpoint)) runBestEffort('bridge-stale-endpoint', () => fs.unlinkSync(this.endpoint));
76
+ }
77
+ this.server = net.createServer(socket => this.accept(socket));
78
+ return new Promise((resolve, reject) => {
79
+ const fail = error => {
80
+ if (this.server) {
81
+ runBestEffort('bridge-start-close', () => this.server.close());
82
+ }
83
+ this.server = null;
84
+ reject(error);
85
+ };
86
+ this.server.once('error', fail);
87
+ this.server.listen(this.endpoint, () => {
88
+ this.server.removeListener('error', fail);
89
+ try {
90
+ safeWriteJson(this.file, this.info());
91
+ this.attachTerminalListeners();
92
+ resolve(this.info());
93
+ } catch (error) {
94
+ fail(error);
95
+ }
96
+ });
97
+ });
98
+ }
99
+
100
+ attachTerminalListeners() {
101
+ if (this.terminalListenersAttached) return;
102
+ this.terminalManager.on('data', this.onTerminalData);
103
+ this.terminalManager.on('state', this.onTerminalState);
104
+ this.terminalListenersAttached = true;
105
+ }
106
+
107
+ detachTerminalListeners() {
108
+ if (!this.terminalManager || !this.terminalListenersAttached) return;
109
+ this.terminalManager.removeListener('data', this.onTerminalData);
110
+ this.terminalManager.removeListener('state', this.onTerminalState);
111
+ this.terminalListenersAttached = false;
112
+ }
113
+
114
+ info() {
115
+ return {
116
+ protocol: PROTOCOL_VERSION,
117
+ endpoint: this.endpoint,
118
+ token: this.token,
119
+ pid: process.pid,
120
+ platform: this.platform,
121
+ updatedAt: new Date().toISOString(),
122
+ };
123
+ }
124
+
125
+ accept(socket) {
126
+ socket.setNoDelay(true);
127
+ const client = { socket, buffer: '', authenticated: false, terminalId: '', bridgeId: '', authTimer: null };
128
+ client.authTimer = setTimeout(() => {
129
+ if (!client.authenticated) socket.destroy(new Error('브리지 인증 시간이 초과되었습니다.'));
130
+ }, AUTH_TIMEOUT_MS);
131
+ this.clients.set(socket, client);
132
+ socket.on('data', chunk => this.consume(client, chunk));
133
+ socket.on('error', () => this.detach(client));
134
+ socket.on('close', () => this.detach(client));
135
+ }
136
+
137
+ consume(client, chunk) {
138
+ client.buffer += chunk.toString('utf8');
139
+ if (client.buffer.length > MAX_FRAME_CHARS) return client.socket.destroy(new Error('브리지 입력이 너무 큽니다.'));
140
+ let newline;
141
+ while ((newline = client.buffer.indexOf('\n')) >= 0) {
142
+ const line = client.buffer.slice(0, newline).trim();
143
+ client.buffer = client.buffer.slice(newline + 1);
144
+ if (!line) continue;
145
+ let message;
146
+ try {
147
+ message = JSON.parse(line);
148
+ } catch (_invalidBridgeFrame) {
149
+ return client.socket.destroy(new Error('브리지 메시지가 올바르지 않습니다.'));
150
+ }
151
+ try { this.handle(client, message || {}); } catch (error) {
152
+ sendFrame(client.socket, { type: 'error', message: String(error.message || error) });
153
+ if (!client.authenticated) client.socket.end();
154
+ }
155
+ }
156
+ }
157
+
158
+ handle(client, message) {
159
+ if (!client.authenticated) {
160
+ if (message.type !== 'run' || message.token !== this.token) throw new Error('LoadToAgent 브리지 인증에 실패했습니다.');
161
+ const provider = validProvider(message.provider);
162
+ if (!provider) throw new Error('지원하지 않는 AI 제공사입니다.');
163
+ const bridgeId = crypto.randomUUID();
164
+ const session = this.terminalManager.create({
165
+ type: 'agent',
166
+ provider,
167
+ args: Array.isArray(message.args) ? message.args : [],
168
+ cwd: message.cwd || this.home,
169
+ title: `외부 연결 · ${AGENT_PROVIDERS[provider].label}`,
170
+ bridgeId,
171
+ cols: message.cols,
172
+ rows: message.rows,
173
+ });
174
+ client.authenticated = true;
175
+ clearTimeout(client.authTimer);
176
+ client.authTimer = null;
177
+ client.terminalId = session.id;
178
+ client.bridgeId = bridgeId;
179
+ sendFrame(client.socket, {
180
+ type: 'started',
181
+ bridgeId,
182
+ terminalId: session.id,
183
+ pid: session.pid,
184
+ replay: Buffer.from(session.replay || '', 'utf8').toString('base64'),
185
+ });
186
+ return;
187
+ }
188
+ if (message.type === 'input') {
189
+ this.terminalManager.write(client.terminalId, decodeBase64(message.data));
190
+ } else if (message.type === 'resize') {
191
+ this.terminalManager.resize(client.terminalId, message.cols, message.rows);
192
+ } else if (message.type === 'signal') {
193
+ this.terminalManager.signal(client.terminalId, message.signal);
194
+ } else if (message.type === 'close') {
195
+ this.terminalManager.close(client.terminalId);
196
+ client.socket.end();
197
+ } else throw new Error('지원하지 않는 브리지 메시지입니다.');
198
+ }
199
+
200
+ forwardData(payload) {
201
+ for (const client of this.clients.values()) {
202
+ if (client.terminalId === payload.id) sendFrame(client.socket, { type: 'output', data: Buffer.from(String(payload.data || ''), 'utf8').toString('base64') });
203
+ }
204
+ }
205
+
206
+ forwardState(payload) {
207
+ const session = payload && payload.session;
208
+ if (!session) return;
209
+ for (const client of this.clients.values()) {
210
+ if (client.terminalId !== session.id) continue;
211
+ sendFrame(client.socket, { type: 'state', status: session.status, exitCode: session.exitCode, signal: session.signal });
212
+ if (session.status === 'exited' || session.status === 'failed') client.socket.end();
213
+ }
214
+ }
215
+
216
+ detach(client) {
217
+ if (client.authTimer) clearTimeout(client.authTimer);
218
+ this.clients.delete(client.socket);
219
+ }
220
+
221
+ dispose() {
222
+ this.detachTerminalListeners();
223
+ for (const client of this.clients.values()) {
224
+ runBestEffort('bridge-client-close', () => client.socket.destroy());
225
+ }
226
+ this.clients.clear();
227
+ if (this.server) {
228
+ runBestEffort('bridge-server-close', () => this.server.close());
229
+ this.server = null;
230
+ }
231
+ if (fs.existsSync(this.file)) runBestEffort('bridge-discovery-cleanup', () => fs.unlinkSync(this.file));
232
+ if (this.platform !== 'win32') {
233
+ if (fs.existsSync(this.endpoint)) runBestEffort('bridge-endpoint-cleanup', () => fs.unlinkSync(this.endpoint));
234
+ }
235
+ }
236
+ }
237
+
238
+ module.exports = {
239
+ BridgeServer,
240
+ PROTOCOL_VERSION,
241
+ bridgeDirectory,
242
+ discoveryFile,
243
+ endpointFor,
244
+ safeWriteJson,
245
+ decodeBase64,
246
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {Object} TokenUsage
5
+ * @property {number} input
6
+ * @property {number} output
7
+ * @property {number} cached
8
+ * @property {number} total
9
+ * @property {number|null} contextWindow
10
+ * @property {number|null} contextUsed
11
+ * @property {number|null} contextPercent
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} CollaborationMetrics
16
+ * @property {number} spawnedTotal Total child sessions observed in the log.
17
+ * @property {number} running Number of child sessions that are currently active.
18
+ * @property {number} completed Number of child sessions with a completion event.
19
+ * @property {number|null} concurrencyLimit Provider concurrency limit when known.
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} CollaborationSummary
24
+ * @property {CollaborationMetrics} metrics
25
+ * @property {Array<Object>} communications Normalized parent/child messages.
26
+ */
27
+
28
+ /**
29
+ * Canonical provider-neutral session exchanged between the monitor worker,
30
+ * preload bridge, and renderer. Provider parsers may retain extra fields, but
31
+ * consumers should depend only on this contract.
32
+ *
33
+ * @typedef {Object} AgentSession
34
+ * @property {string} id
35
+ * @property {'claude'|'codex'|'gemini'|'grok'} provider
36
+ * @property {string} title
37
+ * @property {string} status
38
+ * @property {string|null} parentId
39
+ * @property {string} cwd
40
+ * @property {string} updatedAt
41
+ * @property {TokenUsage} usage
42
+ * @property {Array<Object>} messages
43
+ * @property {Array<Object>} lifecycle
44
+ * @property {CollaborationSummary|null} collaboration
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} TerminalSession
49
+ * @property {string} id
50
+ * @property {'powershell'|'cmd'|'shell'|'wsl'|'tmux'|'agent'} type
51
+ * @property {string} title
52
+ * @property {string} cwd
53
+ * @property {'starting'|'running'|'exited'|'failed'} status
54
+ * @property {number|null} pid
55
+ * @property {string} replay
56
+ */
57
+
58
+ /**
59
+ * Initial renderer payload returned by `app:bootstrap`.
60
+ * @typedef {Object} BootstrapPayload
61
+ * @property {Array<Object>} providers
62
+ * @property {Object<string, boolean|string>} availability
63
+ * @property {Array<{path:string,name:string}>} workspaces
64
+ * @property {{sessions: AgentSession[], summary: Object}} snapshot
65
+ * @property {Array<Object>} activeRuns
66
+ * @property {{app:string,electron:string,node:string}} versions
67
+ * @property {Object} platform
68
+ * @property {Object|null} update
69
+ */
70
+
71
+ module.exports = {};
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Records a recoverable failure without converting a best-effort operation into
5
+ * an application crash. Callers must provide an operation name so warnings are
6
+ * actionable in packaged-app logs.
7
+ */
8
+ function reportRecoverableError(operation, error) {
9
+ const message = error && error.message ? error.message : String(error || 'unknown error');
10
+ console.warn(`[LoadToAgent:${operation}] ${message}`);
11
+ }
12
+
13
+ /** Runs cleanup code and reports a failure that is safe to continue past. */
14
+ function runBestEffort(operation, action) {
15
+ try {
16
+ return action();
17
+ } catch (error) {
18
+ reportRecoverableError(operation, error);
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ module.exports = { reportRecoverableError, runBestEffort };
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ function registerAgentIpc({ handleTrusted, snapshot, requestDetail, runner, probeProviders }) {
4
+ handleTrusted('agents:snapshot', snapshot);
5
+ handleTrusted('agents:detail', requestDetail);
6
+ handleTrusted('agents:run', options => runner().start(options));
7
+ handleTrusted('agents:stop', runId => runner().stop(runId));
8
+ handleTrusted('agents:active-runs', () => runner().listActive());
9
+ handleTrusted('providers:probe', probeProviders);
10
+ }
11
+
12
+ module.exports = { registerAgentIpc };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ function registerAppIpc({ handleTrusted, bootstrap, backgroundState, show, setLocale, updateManager }) {
4
+ handleTrusted('app:bootstrap', bootstrap);
5
+ handleTrusted('app:background-state', backgroundState);
6
+ handleTrusted('app:show', show);
7
+ handleTrusted('app:set-locale', setLocale);
8
+ handleTrusted('app:update-check', () => requireUpdateManager(updateManager).check());
9
+ handleTrusted('app:update-download', () => requireUpdateManager(updateManager).download());
10
+ handleTrusted('app:update-open', () => requireUpdateManager(updateManager).openDownloaded());
11
+ handleTrusted('app:update-open-release', () => requireUpdateManager(updateManager).openReleasePage());
12
+ }
13
+
14
+ function requireUpdateManager(getUpdateManager) {
15
+ const manager = getUpdateManager();
16
+ if (!manager) throw new Error('업데이트 관리자가 준비되지 않았습니다.');
17
+ return manager;
18
+ }
19
+
20
+ module.exports = { registerAppIpc };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function registerTerminalIpc({ ipcMain, requireTrustedSender, trustedSender, manager, listWslDistros, sendError }) {
4
+ ipcMain.handle('terminals:list', event => {
5
+ requireTrustedSender(event);
6
+ return manager() ? manager().list() : [];
7
+ });
8
+ ipcMain.handle('wsl:list-distros', event => {
9
+ requireTrustedSender(event);
10
+ return listWslDistros();
11
+ });
12
+ ipcMain.handle('terminals:get', (event, id) => {
13
+ requireTrustedSender(event);
14
+ return manager() ? manager().get(id, true) : null;
15
+ });
16
+ ipcMain.handle('terminals:create', (event, options) => {
17
+ requireTrustedSender(event);
18
+ return requireManager(manager).create(options || {});
19
+ });
20
+ ipcMain.on('terminals:write', (event, id, data) => {
21
+ if (!trustedSender(event) || !manager()) return;
22
+ try { manager().write(id, data); } catch (error) { sendError({ id: String(id || ''), message: error.message }); }
23
+ });
24
+ ipcMain.handle('terminals:command', (event, id, command) => {
25
+ requireTrustedSender(event);
26
+ return requireManager(manager).command(id, command);
27
+ });
28
+ ipcMain.on('terminals:resize', (event, id, cols, rows) => {
29
+ if (!trustedSender(event) || !manager()) return;
30
+ try {
31
+ manager().resize(id, cols, rows);
32
+ } catch (error) {
33
+ sendError({ id: String(id || ''), message: error.message });
34
+ }
35
+ });
36
+ for (const operation of ['signal', 'restart', 'close']) {
37
+ ipcMain.handle(`terminals:${operation}`, (event, ...args) => {
38
+ requireTrustedSender(event);
39
+ return requireManager(manager)[operation](...args);
40
+ });
41
+ }
42
+ }
43
+
44
+ function requireManager(getManager) {
45
+ const terminalManager = getManager();
46
+ if (!terminalManager) throw new Error('터미널 관리자가 준비되지 않았습니다.');
47
+ return terminalManager;
48
+ }
49
+
50
+ module.exports = { registerTerminalIpc };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const MUTATING_OPERATIONS = [
4
+ 'new-session',
5
+ 'new-window',
6
+ 'split-pane',
7
+ 'rename-session',
8
+ 'rename-window',
9
+ 'select-layout',
10
+ 'kill-pane',
11
+ 'kill-window',
12
+ 'kill-session',
13
+ ];
14
+
15
+ function registerTmuxIpc({ handleTrusted, controller, refresh }) {
16
+ handleTrusted('tmux:send-text', options => controller.sendText(options || {}));
17
+ handleTrusted('tmux:send-key', options => controller.sendKey(options || {}));
18
+ handleTrusted('tmux:capture', options => controller.capture(options || {}));
19
+
20
+ for (const operation of MUTATING_OPERATIONS) {
21
+ const method = operation.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
22
+ handleTrusted(`tmux:${operation}`, async options => {
23
+ const result = await controller[method](options || {});
24
+ refresh();
25
+ return result;
26
+ });
27
+ }
28
+ }
29
+
30
+ module.exports = { registerTmuxIpc };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ function registerWorkspaceIpc({ handleTrusted, list, add, remove, pick, openExternal, writeClipboard, bridgeCommand, openOrigin }) {
4
+ handleTrusted('workspaces:list', list);
5
+ handleTrusted('workspaces:add', add);
6
+ handleTrusted('workspaces:remove', remove);
7
+ handleTrusted('workspaces:pick', pick);
8
+ handleTrusted('external:open', openExternal);
9
+ handleTrusted('clipboard:write', writeClipboard);
10
+ handleTrusted('bridge:command', bridgeCommand);
11
+ handleTrusted('agents:open-origin', openOrigin);
12
+ }
13
+
14
+ module.exports = { registerWorkspaceIpc };