openwork 0.1.1-rc.4 → 0.1.1-rc.6

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,29 +1,23 @@
1
1
  # openwork
2
2
 
3
- [![CI](https://github.com/langchain-ai/openwork/actions/workflows/ci.yml/badge.svg)](https://github.com/langchain-ai/openwork/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/openwork.svg)](https://www.npmjs.com/package/openwork)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ [![npm][npm-badge]][npm-url] [![License: MIT][license-badge]][license-url]
6
4
 
7
- A tactical agent interface for [deepagentsjs](https://github.com/langchain-ai/deepagentsjs) - an opinionated harness for building deep agents with filesystem capabilities, planning, and subagent delegation.
5
+ [npm-badge]: https://img.shields.io/npm/v/openwork.svg
6
+ [npm-url]: https://www.npmjs.com/package/openwork
7
+ [license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
8
+ [license-url]: https://opensource.org/licenses/MIT
8
9
 
9
- ![openwork screenshot](docs/screenshot.png)
10
-
11
- ## Features
10
+ A desktop interface for [deepagentsjs](https://github.com/langchain-ai/deepagentsjs) — an opinionated harness for building deep agents with filesystem capabilities planning, and subagent delegation.
12
11
 
13
- - **Chat Interface** - Stream conversations with your AI agent in real-time
14
- - **TODO Tracking** - Visual task list showing agent's planning progress
15
- - **Filesystem Browser** - See files the agent reads, writes, and edits
16
- - **Subagent Monitoring** - Track spawned subagents and their status
17
- - **Human-in-the-Loop** - Approve, edit, or reject sensitive tool calls
18
- - **Multi-Model Support** - Use Claude, GPT-4, Gemini, or local models
19
- - **Thread Persistence** - SQLite-backed conversation history
12
+ ![openwork screenshot](docs/screenshot.png)
20
13
 
21
- ## Installation
14
+ > [!CAUTION]
15
+ > openwork gives AI agents direct access to your filesystem and the ability to execute shell commands. Always review tool calls before approving them, and only run in workspaces you trust.
22
16
 
23
- ### npm (recommended)
17
+ ## Get Started
24
18
 
25
19
  ```bash
26
- # Run directly
20
+ # Run directly with npx
27
21
  npx openwork
28
22
 
29
23
  # Or install globally
@@ -31,7 +25,7 @@ npm install -g openwork
31
25
  openwork
32
26
  ```
33
27
 
34
- Requires Node.js 18+. Electron is installed automatically as a dependency.
28
+ Requires Node.js 18+.
35
29
 
36
30
  ### From Source
37
31
 
@@ -41,103 +35,21 @@ cd openwork
41
35
  npm install
42
36
  npm run dev
43
37
  ```
38
+ Or configure them in-app via the settings panel.
44
39
 
45
- ## Configuration
46
-
47
- ### API Keys
48
-
49
- openwork supports multiple LLM providers. Set your API keys via:
50
-
51
- 1. **Environment Variables** (recommended)
52
- ```bash
53
- export ANTHROPIC_API_KEY="sk-ant-..."
54
- export OPENAI_API_KEY="sk-..."
55
- export GOOGLE_API_KEY="..."
56
- ```
57
-
58
- 2. **In-App Settings** - Click the settings icon and enter your API keys securely.
59
-
60
- ### Supported Models
61
-
62
- | Provider | Models |
63
- |----------|--------|
64
- | Anthropic | Claude Sonnet 4, Claude 3.5 Sonnet, Claude 3.5 Haiku |
65
- | OpenAI | GPT-4o, GPT-4o Mini |
66
- | Google | Gemini 2.0 Flash |
67
-
68
- ## Architecture
69
-
70
- openwork is built with:
71
-
72
- - **Electron** - Cross-platform desktop framework
73
- - **React** - UI components with tactical/SCADA-inspired design
74
- - **deepagentsjs** - Agent harness with planning, filesystem, and subagents
75
- - **LangGraph** - State machine for agent orchestration
76
- - **SQLite** - Local persistence for threads and checkpoints
77
-
78
- ```
79
- ┌─────────────────────────────────────────────────────────────┐
80
- │ Electron Main Process │
81
- ├─────────────────────────────────────────────────────────────┤
82
- │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
83
- │ │ IPC Handlers│ │ SQLite │ │ DeepAgentsJS │ │
84
- │ │ - agent │ │ - threads │ │ - createAgent │ │
85
- │ │ - threads │ │ - runs │ │ - checkpointer │ │
86
- │ │ - models │ │ - assists │ │ - middleware │ │
87
- │ └─────────────┘ └─────────────┘ └─────────────────────┘ │
88
- └─────────────────────────────────────────────────────────────┘
89
-
90
- IPC Bridge
91
-
92
- ┌─────────────────────────────────────────────────────────────┐
93
- │ Electron Renderer Process │
94
- ├─────────────────────────────────────────────────────────────┤
95
- │ ┌──────────┐ ┌─────────────────────┐ ┌───────────────┐ │
96
- │ │ Sidebar │ │ Chat Interface │ │ Right Panel │ │
97
- │ │ - Threads│ │ - Messages │ │ - TODOs │ │
98
- │ │ - Model │ │ - Tool Renderers │ │ - Files │ │
99
- │ │ - Config │ │ - Streaming │ │ - Subagents │ │
100
- │ └──────────┘ └─────────────────────┘ └───────────────┘ │
101
- └─────────────────────────────────────────────────────────────┘
102
- ```
40
+ ## Supported Models
103
41
 
104
- ## Development
105
-
106
- ```bash
107
- # Install dependencies
108
- npm install
109
-
110
- # Start development server
111
- npm run dev
112
-
113
- # Build for production
114
- npm run build
115
- ```
116
-
117
- ## Releases
118
-
119
- To publish a new release:
120
-
121
- 1. Create a git tag: `git tag v0.2.0`
122
- 2. Push the tag: `git push origin v0.2.0`
123
- 3. GitHub Actions will:
124
- - Build the application
125
- - Publish to npm
126
- - Create a GitHub release
127
-
128
- ## Design System
129
-
130
- openwork uses a tactical/SCADA-inspired design system optimized for:
131
-
132
- - **Information density** - Dense layouts for monitoring agent activity
133
- - **Status at a glance** - Color-coded status indicators (nominal, warning, critical)
134
- - **Dark mode only** - Reduced eye strain for extended sessions
135
- - **Monospace typography** - JetBrains Mono for data and code
42
+ | Provider | Models |
43
+ | --------- | ----------------------------------------------------------------- |
44
+ | Anthropic | Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5, Claude Opus 4.1, Claude Sonnet 4 |
45
+ | OpenAI | GPT-5.2, GPT-5.1, o3, o3 Mini, o4 Mini, o1, GPT-4.1, GPT-4o |
136
46
 
137
47
  ## Contributing
138
48
 
139
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
49
+ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
50
+
51
+ Report bugs via [GitHub Issues](https://github.com/langchain-ai/openwork/issues).
140
52
 
141
53
  ## License
142
54
 
143
- MIT License - see [LICENSE](LICENSE) for details.
55
+ MIT see [LICENSE](LICENSE) for details.
package/out/main/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  const electron = require("electron");
3
3
  const path = require("path");
4
4
  const messages = require("@langchain/core/messages");
5
+ const langgraph = require("@langchain/langgraph");
5
6
  const deepagents = require("deepagents");
6
7
  const Store = require("electron-store");
7
8
  const fs$1 = require("fs/promises");
@@ -11,6 +12,8 @@ const anthropic = require("@langchain/anthropic");
11
12
  const openai = require("@langchain/openai");
12
13
  const initSqlJs = require("sql.js");
13
14
  const langgraphCheckpoint = require("@langchain/langgraph-checkpoint");
15
+ const node_child_process = require("node:child_process");
16
+ const node_crypto = require("node:crypto");
14
17
  const uuid = require("uuid");
15
18
  function _interopNamespaceDefault(e) {
16
19
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -817,6 +820,139 @@ class SqlJsSaver extends langgraphCheckpoint.BaseCheckpointSaver {
817
820
  }
818
821
  }
819
822
  }
823
+ class LocalSandbox extends deepagents.FilesystemBackend {
824
+ /** Unique identifier for this sandbox instance */
825
+ id;
826
+ timeout;
827
+ maxOutputBytes;
828
+ env;
829
+ workingDir;
830
+ constructor(options = {}) {
831
+ super({
832
+ rootDir: options.rootDir,
833
+ virtualMode: options.virtualMode,
834
+ maxFileSizeMb: options.maxFileSizeMb
835
+ });
836
+ this.id = `local-sandbox-${node_crypto.randomUUID().slice(0, 8)}`;
837
+ this.timeout = options.timeout ?? 12e4;
838
+ this.maxOutputBytes = options.maxOutputBytes ?? 1e5;
839
+ this.env = options.env ?? { ...process.env };
840
+ this.workingDir = options.rootDir ?? process.cwd();
841
+ }
842
+ /**
843
+ * Execute a shell command in the workspace directory.
844
+ *
845
+ * @param command - Shell command string to execute
846
+ * @returns ExecuteResponse with combined output, exit code, and truncation flag
847
+ *
848
+ * @example
849
+ * ```typescript
850
+ * const result = await sandbox.execute('echo "Hello World"');
851
+ * // result.output: "Hello World\n"
852
+ * // result.exitCode: 0
853
+ * // result.truncated: false
854
+ * ```
855
+ */
856
+ async execute(command) {
857
+ if (!command || typeof command !== "string") {
858
+ return {
859
+ output: "Error: Shell tool expects a non-empty command string.",
860
+ exitCode: 1,
861
+ truncated: false
862
+ };
863
+ }
864
+ return new Promise((resolve) => {
865
+ const outputParts = [];
866
+ let totalBytes = 0;
867
+ let truncated = false;
868
+ let resolved = false;
869
+ const isWindows = process.platform === "win32";
870
+ const shell = isWindows ? "cmd.exe" : "/bin/sh";
871
+ const shellArgs = isWindows ? ["/c", command] : ["-c", command];
872
+ const proc = node_child_process.spawn(shell, shellArgs, {
873
+ cwd: this.workingDir,
874
+ env: this.env,
875
+ stdio: ["ignore", "pipe", "pipe"]
876
+ });
877
+ const timeoutId = setTimeout(() => {
878
+ if (!resolved) {
879
+ resolved = true;
880
+ proc.kill("SIGTERM");
881
+ setTimeout(() => proc.kill("SIGKILL"), 1e3);
882
+ resolve({
883
+ output: `Error: Command timed out after ${(this.timeout / 1e3).toFixed(1)} seconds.`,
884
+ exitCode: null,
885
+ truncated: false
886
+ });
887
+ }
888
+ }, this.timeout);
889
+ proc.stdout.on("data", (data) => {
890
+ if (truncated) return;
891
+ const chunk = data.toString();
892
+ const newTotal = totalBytes + chunk.length;
893
+ if (newTotal > this.maxOutputBytes) {
894
+ const remaining = this.maxOutputBytes - totalBytes;
895
+ if (remaining > 0) {
896
+ outputParts.push(chunk.slice(0, remaining));
897
+ }
898
+ truncated = true;
899
+ totalBytes = this.maxOutputBytes;
900
+ } else {
901
+ outputParts.push(chunk);
902
+ totalBytes = newTotal;
903
+ }
904
+ });
905
+ proc.stderr.on("data", (data) => {
906
+ if (truncated) return;
907
+ const chunk = data.toString();
908
+ const prefixedLines = chunk.split("\n").filter((line) => line.length > 0).map((line) => `[stderr] ${line}`).join("\n");
909
+ if (prefixedLines.length === 0) return;
910
+ const withNewline = prefixedLines + (chunk.endsWith("\n") ? "\n" : "");
911
+ const newTotal = totalBytes + withNewline.length;
912
+ if (newTotal > this.maxOutputBytes) {
913
+ const remaining = this.maxOutputBytes - totalBytes;
914
+ if (remaining > 0) {
915
+ outputParts.push(withNewline.slice(0, remaining));
916
+ }
917
+ truncated = true;
918
+ totalBytes = this.maxOutputBytes;
919
+ } else {
920
+ outputParts.push(withNewline);
921
+ totalBytes = newTotal;
922
+ }
923
+ });
924
+ proc.on("close", (code, signal) => {
925
+ if (resolved) return;
926
+ resolved = true;
927
+ clearTimeout(timeoutId);
928
+ let output = outputParts.join("");
929
+ if (truncated) {
930
+ output += `
931
+
932
+ ... Output truncated at ${this.maxOutputBytes} bytes.`;
933
+ }
934
+ if (!output.trim()) {
935
+ output = "<no output>";
936
+ }
937
+ resolve({
938
+ output,
939
+ exitCode: signal ? null : code,
940
+ truncated
941
+ });
942
+ });
943
+ proc.on("error", (err) => {
944
+ if (resolved) return;
945
+ resolved = true;
946
+ clearTimeout(timeoutId);
947
+ resolve({
948
+ output: `Error: Failed to execute command: ${err.message}`,
949
+ exitCode: 1,
950
+ truncated: false
951
+ });
952
+ });
953
+ });
954
+ }
955
+ }
820
956
  const BASE_SYSTEM_PROMPT = `You are an AI assistant that helps users with various tasks including coding, research, and analysis.
821
957
 
822
958
  # Core Behavior
@@ -871,11 +1007,27 @@ When delegating to subagents:
871
1007
  - read_file: Read file contents
872
1008
  - edit_file: Replace exact strings in files (must read first, provide unique old_string)
873
1009
  - write_file: Create or overwrite files
874
- - ls: List directory contents (use "/" for workspace root)
1010
+ - ls: List directory contents
875
1011
  - glob: Find files by pattern (e.g., "**/*.py")
876
1012
  - grep: Search file contents
877
1013
 
878
- All file paths are virtual paths relative to the workspace root, starting with /.
1014
+ All file paths should use fully qualified absolute system paths (e.g., /Users/name/project/src/file.ts).
1015
+
1016
+ ### Shell Tool
1017
+ - execute: Run shell commands in the workspace directory
1018
+
1019
+ The execute tool runs commands directly on the user's machine. Use it for:
1020
+ - Running scripts, tests, and builds (npm test, python script.py, make)
1021
+ - Git operations (git status, git diff, git commit)
1022
+ - Installing dependencies (npm install, pip install)
1023
+ - System commands (which, env, pwd)
1024
+
1025
+ **Important:**
1026
+ - All execute commands require user approval before running
1027
+ - Commands run in the workspace root directory
1028
+ - Avoid using shell for file reading (use read_file instead)
1029
+ - Avoid using shell for file searching (use grep/glob instead)
1030
+ - When running non-trivial commands, briefly explain what they do
879
1031
 
880
1032
  ## Code References
881
1033
  When referencing code, use format: \`file_path:line_number\`
@@ -915,11 +1067,11 @@ function getSystemPrompt(workspacePath) {
915
1067
  ### File System and Paths
916
1068
 
917
1069
  **IMPORTANT - Path Handling:**
918
- - All file paths use virtual paths starting with \`/\` (the workspace root)
919
- - \`/\` refers to the workspace root directory
920
- - Example: \`/src/index.ts\`, \`/README.md\`, \`/package.json\`
921
- - To list the workspace root, use \`ls("/")\`
922
- - Never use fully qualified system paths like \`${workspacePath}/...\`
1070
+ - All file paths use fully qualified absolute system paths
1071
+ - The workspace root is: \`${workspacePath}\`
1072
+ - Example: \`${workspacePath}/src/index.ts\`, \`${workspacePath}/README.md\`
1073
+ - To list the workspace root, use \`ls("${workspacePath}")\`
1074
+ - Always use full absolute paths for all file operations
923
1075
  `;
924
1076
  return workingDirSection + BASE_SYSTEM_PROMPT;
925
1077
  }
@@ -970,18 +1122,37 @@ async function createAgentRuntime(options) {
970
1122
  console.log("[Runtime] Model instance created:", typeof model);
971
1123
  const checkpointer2 = await getCheckpointer();
972
1124
  console.log("[Runtime] Checkpointer ready");
973
- const backend = new deepagents.FilesystemBackend({
1125
+ const backend = new LocalSandbox({
974
1126
  rootDir: workspacePath,
975
- virtualMode: true
1127
+ virtualMode: false,
1128
+ // Use absolute system paths for consistency with shell commands
1129
+ timeout: 12e4,
1130
+ // 2 minutes
1131
+ maxOutputBytes: 1e5
1132
+ // ~100KB
976
1133
  });
977
1134
  const systemPrompt = getSystemPrompt(workspacePath);
1135
+ const filesystemSystemPrompt = `You have access to a filesystem. All file paths use fully qualified absolute system paths.
1136
+
1137
+ - ls: list files in a directory (e.g., ls("${workspacePath}"))
1138
+ - read_file: read a file from the filesystem
1139
+ - write_file: write to a file in the filesystem
1140
+ - edit_file: edit a file in the filesystem
1141
+ - glob: find files matching a pattern (e.g., "**/*.py")
1142
+ - grep: search for text within files
1143
+
1144
+ The workspace root is: ${workspacePath}`;
978
1145
  const agent = deepagents.createDeepAgent({
979
1146
  model,
980
1147
  checkpointer: checkpointer2,
981
1148
  backend,
982
- systemPrompt
1149
+ systemPrompt,
1150
+ // Custom filesystem prompt for absolute paths (requires deepagents update)
1151
+ filesystemSystemPrompt,
1152
+ // Require human approval for all shell commands
1153
+ interruptOn: { execute: true }
983
1154
  });
984
- console.log("[Runtime] Deep agent created with FilesystemBackend at:", workspacePath);
1155
+ console.log("[Runtime] Deep agent created with LocalSandbox at:", workspacePath);
985
1156
  return agent;
986
1157
  }
987
1158
  let db = null;
@@ -1222,19 +1393,125 @@ function registerAgentHandlers(ipcMain) {
1222
1393
  }
1223
1394
  }
1224
1395
  );
1225
- ipcMain.handle(
1396
+ ipcMain.on(
1397
+ "agent:resume",
1398
+ async (event, {
1399
+ threadId,
1400
+ command
1401
+ }) => {
1402
+ const channel = `agent:stream:${threadId}`;
1403
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1404
+ console.log("[Agent] Received resume request:", { threadId, command });
1405
+ if (!window) {
1406
+ console.error("[Agent] No window found for resume");
1407
+ return;
1408
+ }
1409
+ const thread = getThread(threadId);
1410
+ const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
1411
+ const workspacePath = metadata.workspacePath;
1412
+ if (!workspacePath) {
1413
+ window.webContents.send(channel, {
1414
+ type: "error",
1415
+ error: "Workspace path is required"
1416
+ });
1417
+ return;
1418
+ }
1419
+ const existingController = activeRuns.get(threadId);
1420
+ if (existingController) {
1421
+ existingController.abort();
1422
+ activeRuns.delete(threadId);
1423
+ }
1424
+ const abortController = new AbortController();
1425
+ activeRuns.set(threadId, abortController);
1426
+ try {
1427
+ const agent = await createAgentRuntime({ workspacePath });
1428
+ const config = {
1429
+ configurable: { thread_id: threadId },
1430
+ signal: abortController.signal,
1431
+ streamMode: ["messages", "values"],
1432
+ recursionLimit: 1e3
1433
+ };
1434
+ const decisionType = command?.resume?.decision || "approve";
1435
+ const resumeValue = { decisions: [{ type: decisionType }] };
1436
+ const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
1437
+ for await (const chunk of stream) {
1438
+ if (abortController.signal.aborted) break;
1439
+ const [mode, data] = chunk;
1440
+ window.webContents.send(channel, {
1441
+ type: "stream",
1442
+ mode,
1443
+ data: JSON.parse(JSON.stringify(data))
1444
+ });
1445
+ }
1446
+ window.webContents.send(channel, { type: "done" });
1447
+ } catch (error) {
1448
+ console.error("[Agent] Resume error:", error);
1449
+ window.webContents.send(channel, {
1450
+ type: "error",
1451
+ error: error instanceof Error ? error.message : "Unknown error"
1452
+ });
1453
+ } finally {
1454
+ activeRuns.delete(threadId);
1455
+ }
1456
+ }
1457
+ );
1458
+ ipcMain.on(
1226
1459
  "agent:interrupt",
1227
- async (_event, { threadId, decision }) => {
1460
+ async (event, { threadId, decision }) => {
1461
+ const channel = `agent:stream:${threadId}`;
1462
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1463
+ if (!window) {
1464
+ console.error("[Agent] No window found for interrupt response");
1465
+ return;
1466
+ }
1228
1467
  const thread = getThread(threadId);
1229
1468
  const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
1230
1469
  const workspacePath = metadata.workspacePath;
1231
1470
  if (!workspacePath) {
1232
- throw new Error("Workspace path is required");
1471
+ window.webContents.send(channel, {
1472
+ type: "error",
1473
+ error: "Workspace path is required"
1474
+ });
1475
+ return;
1233
1476
  }
1234
- const agent = await createAgentRuntime({ workspacePath });
1235
- const config = { configurable: { thread_id: threadId } };
1236
- if (decision.type === "approve") {
1237
- await agent.invoke(null, config);
1477
+ const existingController = activeRuns.get(threadId);
1478
+ if (existingController) {
1479
+ existingController.abort();
1480
+ activeRuns.delete(threadId);
1481
+ }
1482
+ const abortController = new AbortController();
1483
+ activeRuns.set(threadId, abortController);
1484
+ try {
1485
+ const agent = await createAgentRuntime({ workspacePath });
1486
+ const config = {
1487
+ configurable: { thread_id: threadId },
1488
+ signal: abortController.signal,
1489
+ streamMode: ["messages", "values"],
1490
+ recursionLimit: 1e3
1491
+ };
1492
+ if (decision.type === "approve") {
1493
+ const stream = await agent.stream(null, config);
1494
+ for await (const chunk of stream) {
1495
+ if (abortController.signal.aborted) break;
1496
+ const [mode, data] = chunk;
1497
+ window.webContents.send(channel, {
1498
+ type: "stream",
1499
+ mode,
1500
+ data: JSON.parse(JSON.stringify(data))
1501
+ });
1502
+ }
1503
+ window.webContents.send(channel, { type: "done" });
1504
+ } else if (decision.type === "reject") {
1505
+ window.webContents.send(channel, { type: "done" });
1506
+ }
1507
+ } catch (error) {
1508
+ console.error("[Agent] Interrupt error:", error);
1509
+ window.webContents.send(channel, {
1510
+ type: "error",
1511
+ error: error instanceof Error ? error.message : "Unknown error"
1512
+ });
1513
+ } finally {
1514
+ activeRuns.delete(threadId);
1238
1515
  }
1239
1516
  }
1240
1517
  );
@@ -21,17 +21,14 @@ const api = {
21
21
  agent: {
22
22
  // Send message and receive events via callback
23
23
  invoke: (threadId, message, onEvent) => {
24
- console.log("[Preload] invoke() called", { threadId, message: message.substring(0, 50) });
25
24
  const channel = `agent:stream:${threadId}`;
26
25
  const handler = (_, data) => {
27
- console.log("[Preload] Received event:", data.type);
28
26
  onEvent(data);
29
27
  if (data.type === "done" || data.type === "error") {
30
28
  electron.ipcRenderer.removeListener(channel, handler);
31
29
  }
32
30
  };
33
31
  electron.ipcRenderer.on(channel, handler);
34
- console.log("[Preload] Sending agent:invoke IPC");
35
32
  electron.ipcRenderer.send("agent:invoke", { threadId, message });
36
33
  return () => {
37
34
  electron.ipcRenderer.removeListener(channel, handler);
@@ -39,10 +36,8 @@ const api = {
39
36
  },
40
37
  // Stream agent events for useStream transport
41
38
  streamAgent: (threadId, message, command, onEvent) => {
42
- console.log("[Preload] streamAgent() called", { threadId, message: message.substring(0, 50) });
43
39
  const channel = `agent:stream:${threadId}`;
44
40
  const handler = (_, data) => {
45
- console.log("[Preload] Received stream event:", data.type);
46
41
  onEvent(data);
47
42
  if (data.type === "done" || data.type === "error") {
48
43
  electron.ipcRenderer.removeListener(channel, handler);
@@ -50,18 +45,27 @@ const api = {
50
45
  };
51
46
  electron.ipcRenderer.on(channel, handler);
52
47
  if (command) {
53
- console.log("[Preload] Sending agent:resume IPC");
54
48
  electron.ipcRenderer.send("agent:resume", { threadId, command });
55
49
  } else {
56
- console.log("[Preload] Sending agent:invoke IPC");
57
50
  electron.ipcRenderer.send("agent:invoke", { threadId, message });
58
51
  }
59
52
  return () => {
60
53
  electron.ipcRenderer.removeListener(channel, handler);
61
54
  };
62
55
  },
63
- interrupt: (threadId, decision) => {
64
- return electron.ipcRenderer.invoke("agent:interrupt", { threadId, decision });
56
+ interrupt: (threadId, decision, onEvent) => {
57
+ const channel = `agent:stream:${threadId}`;
58
+ const handler = (_, data) => {
59
+ onEvent?.(data);
60
+ if (data.type === "done" || data.type === "error") {
61
+ electron.ipcRenderer.removeListener(channel, handler);
62
+ }
63
+ };
64
+ electron.ipcRenderer.on(channel, handler);
65
+ electron.ipcRenderer.send("agent:interrupt", { threadId, decision });
66
+ return () => {
67
+ electron.ipcRenderer.removeListener(channel, handler);
68
+ };
65
69
  },
66
70
  cancel: (threadId) => {
67
71
  return electron.ipcRenderer.invoke("agent:cancel", { threadId });