openwork 0.1.3 → 0.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 +5 -4
- package/bin/cli.js +15 -14
- package/out/main/index.js +201 -227
- package/out/preload/index.js +5 -5
- package/out/renderer/assets/{index-DQkbeNKF.css → index-CeMSVFwO.css} +86 -17
- package/out/renderer/assets/{index-nnGibHq-.js → index-D1IgUk4g.js} +925 -513
- package/out/renderer/index.html +2 -2
- package/package.json +8 -7
- package/resources/README.md +0 -16
package/README.md
CHANGED
|
@@ -35,15 +35,16 @@ cd openwork
|
|
|
35
35
|
npm install
|
|
36
36
|
npm run dev
|
|
37
37
|
```
|
|
38
|
+
|
|
38
39
|
Or configure them in-app via the settings panel.
|
|
39
40
|
|
|
40
41
|
## Supported Models
|
|
41
42
|
|
|
42
|
-
| Provider | Models
|
|
43
|
-
| --------- |
|
|
43
|
+
| Provider | Models |
|
|
44
|
+
| --------- | -------------------------------------------------------------------------------------- |
|
|
44
45
|
| 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
|
|
46
|
-
| Google | Gemini 3 Pro Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite |
|
|
46
|
+
| OpenAI | GPT-5.2, GPT-5.1, o3, o3 Mini, o4 Mini, o1, GPT-4.1, GPT-4o |
|
|
47
|
+
| Google | Gemini 3 Pro Preview, Gemini 3 Flash Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite |
|
|
47
48
|
|
|
48
49
|
## Contributing
|
|
49
50
|
|
package/bin/cli.js
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
* openwork CLI - Launches the Electron app
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { spawn } = require(
|
|
8
|
-
const path = require(
|
|
7
|
+
const { spawn } = require("child_process")
|
|
8
|
+
const path = require("path")
|
|
9
9
|
|
|
10
10
|
// Set process title for Activity Monitor
|
|
11
|
-
process.title =
|
|
11
|
+
process.title = "openwork"
|
|
12
12
|
|
|
13
13
|
const args = process.argv.slice(2)
|
|
14
14
|
|
|
15
15
|
// Handle --version flag
|
|
16
|
-
if (args.includes(
|
|
17
|
-
const { version } = require(
|
|
16
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
17
|
+
const { version } = require("../package.json")
|
|
18
18
|
console.log(`openwork v${version}`)
|
|
19
19
|
process.exit(0)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Handle --help flag
|
|
23
|
-
if (args.includes(
|
|
23
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
24
24
|
console.log(`
|
|
25
25
|
openwork - A tactical agent interface for deepagentsjs
|
|
26
26
|
|
|
@@ -33,31 +33,32 @@ Usage:
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Get the path to electron
|
|
36
|
-
const electron = require(
|
|
36
|
+
const electron = require("electron")
|
|
37
37
|
|
|
38
38
|
// Launch electron with our main process
|
|
39
|
-
const mainPath = path.join(__dirname,
|
|
39
|
+
const mainPath = path.join(__dirname, "..", "out", "main", "index.js")
|
|
40
40
|
|
|
41
41
|
const child = spawn(electron, [mainPath, ...args], {
|
|
42
|
-
stdio:
|
|
42
|
+
stdio: "inherit"
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
// Forward signals to child process
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
46
47
|
function forwardSignal(signal) {
|
|
47
48
|
if (child.pid) {
|
|
48
49
|
process.kill(child.pid, signal)
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
process.on(
|
|
53
|
-
process.on(
|
|
53
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"))
|
|
54
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"))
|
|
54
55
|
|
|
55
56
|
// Exit with the same code as the child
|
|
56
|
-
child.on(
|
|
57
|
+
child.on("close", (code) => {
|
|
57
58
|
process.exit(code ?? 0)
|
|
58
59
|
})
|
|
59
60
|
|
|
60
|
-
child.on(
|
|
61
|
-
console.error(
|
|
61
|
+
child.on("error", (err) => {
|
|
62
|
+
console.error("Failed to start openwork:", err.message)
|
|
62
63
|
process.exit(1)
|
|
63
64
|
})
|
package/out/main/index.js
CHANGED
|
@@ -106,7 +106,9 @@ const ENV_FILE = path.join(OPENWORK_DIR, ".env");
|
|
|
106
106
|
const ENV_VAR_NAMES = {
|
|
107
107
|
anthropic: "ANTHROPIC_API_KEY",
|
|
108
108
|
openai: "OPENAI_API_KEY",
|
|
109
|
-
google: "GOOGLE_API_KEY"
|
|
109
|
+
google: "GOOGLE_API_KEY",
|
|
110
|
+
ollama: ""
|
|
111
|
+
// Ollama doesn't require an API key
|
|
110
112
|
};
|
|
111
113
|
function getOpenworkDir() {
|
|
112
114
|
if (!fs.existsSync(OPENWORK_DIR)) {
|
|
@@ -155,7 +157,7 @@ function parseEnvFile() {
|
|
|
155
157
|
}
|
|
156
158
|
function writeEnvFile(env) {
|
|
157
159
|
getOpenworkDir();
|
|
158
|
-
const lines = Object.entries(env).filter((
|
|
160
|
+
const lines = Object.entries(env).filter((entry) => entry[1]).map(([k, v]) => `${k}=${v}`);
|
|
159
161
|
fs.writeFileSync(getEnvFilePath(), lines.join("\n") + "\n");
|
|
160
162
|
}
|
|
161
163
|
function getApiKey(provider) {
|
|
@@ -336,6 +338,14 @@ const AVAILABLE_MODELS = [
|
|
|
336
338
|
description: "State-of-the-art reasoning and multimodal understanding",
|
|
337
339
|
available: true
|
|
338
340
|
},
|
|
341
|
+
{
|
|
342
|
+
id: "gemini-3-flash-preview",
|
|
343
|
+
name: "Gemini 3 Flash Preview",
|
|
344
|
+
provider: "google",
|
|
345
|
+
model: "gemini-3-flash-preview",
|
|
346
|
+
description: "Fast frontier-class model with low latency and cost",
|
|
347
|
+
available: true
|
|
348
|
+
},
|
|
339
349
|
{
|
|
340
350
|
id: "gemini-2.5-pro",
|
|
341
351
|
name: "Gemini 2.5 Pro",
|
|
@@ -374,12 +384,9 @@ function registerModelHandlers(ipcMain) {
|
|
|
374
384
|
ipcMain.handle("models:setDefault", async (_event, modelId) => {
|
|
375
385
|
store.set("defaultModel", modelId);
|
|
376
386
|
});
|
|
377
|
-
ipcMain.handle(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
setApiKey(provider, apiKey);
|
|
381
|
-
}
|
|
382
|
-
);
|
|
387
|
+
ipcMain.handle("models:setApiKey", async (_event, { provider, apiKey }) => {
|
|
388
|
+
setApiKey(provider, apiKey);
|
|
389
|
+
});
|
|
383
390
|
ipcMain.handle("models:getApiKey", async (_event, provider) => {
|
|
384
391
|
return getApiKey(provider) ?? null;
|
|
385
392
|
});
|
|
@@ -1457,123 +1464,177 @@ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
|
|
|
1457
1464
|
const activeRuns = /* @__PURE__ */ new Map();
|
|
1458
1465
|
function registerAgentHandlers(ipcMain) {
|
|
1459
1466
|
console.log("[Agent] Registering agent handlers...");
|
|
1460
|
-
ipcMain.on(
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1465
|
-
console.log("[Agent] Received invoke request:", {
|
|
1466
|
-
threadId,
|
|
1467
|
-
message: message.substring(0, 50)
|
|
1468
|
-
});
|
|
1469
|
-
if (!window) {
|
|
1470
|
-
console.error("[Agent] No window found");
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
const existingController = activeRuns.get(threadId);
|
|
1474
|
-
if (existingController) {
|
|
1475
|
-
console.log("[Agent] Aborting existing stream for thread:", threadId);
|
|
1476
|
-
existingController.abort();
|
|
1477
|
-
activeRuns.delete(threadId);
|
|
1478
|
-
}
|
|
1479
|
-
const abortController = new AbortController();
|
|
1480
|
-
activeRuns.set(threadId, abortController);
|
|
1481
|
-
const onWindowClosed = () => {
|
|
1482
|
-
console.log("[Agent] Window closed, aborting stream for thread:", threadId);
|
|
1483
|
-
abortController.abort();
|
|
1484
|
-
};
|
|
1485
|
-
window.once("closed", onWindowClosed);
|
|
1486
|
-
try {
|
|
1487
|
-
const thread = getThread(threadId);
|
|
1488
|
-
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1489
|
-
const workspacePath = metadata.workspacePath;
|
|
1490
|
-
if (!workspacePath) {
|
|
1491
|
-
window.webContents.send(channel, {
|
|
1492
|
-
type: "error",
|
|
1493
|
-
error: "WORKSPACE_REQUIRED",
|
|
1494
|
-
message: "Please select a workspace folder before sending messages."
|
|
1495
|
-
});
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
const agent = await createAgentRuntime({ threadId, workspacePath });
|
|
1499
|
-
const humanMessage = new messages.HumanMessage(message);
|
|
1500
|
-
const stream = await agent.stream(
|
|
1501
|
-
{ messages: [humanMessage] },
|
|
1502
|
-
{
|
|
1503
|
-
configurable: { thread_id: threadId },
|
|
1504
|
-
signal: abortController.signal,
|
|
1505
|
-
streamMode: ["messages", "values"],
|
|
1506
|
-
recursionLimit: 1e3
|
|
1507
|
-
}
|
|
1508
|
-
);
|
|
1509
|
-
for await (const chunk of stream) {
|
|
1510
|
-
if (abortController.signal.aborted) break;
|
|
1511
|
-
const [mode, data] = chunk;
|
|
1512
|
-
window.webContents.send(channel, {
|
|
1513
|
-
type: "stream",
|
|
1514
|
-
mode,
|
|
1515
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
if (!abortController.signal.aborted) {
|
|
1519
|
-
window.webContents.send(channel, { type: "done" });
|
|
1520
|
-
}
|
|
1521
|
-
} catch (error) {
|
|
1522
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1523
|
-
if (!isAbortError) {
|
|
1524
|
-
console.error("[Agent] Error:", error);
|
|
1525
|
-
window.webContents.send(channel, {
|
|
1526
|
-
type: "error",
|
|
1527
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
} finally {
|
|
1531
|
-
window.removeListener("closed", onWindowClosed);
|
|
1532
|
-
activeRuns.delete(threadId);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
);
|
|
1536
|
-
ipcMain.on(
|
|
1537
|
-
"agent:resume",
|
|
1538
|
-
async (event, {
|
|
1467
|
+
ipcMain.on("agent:invoke", async (event, { threadId, message, modelId }) => {
|
|
1468
|
+
const channel = `agent:stream:${threadId}`;
|
|
1469
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1470
|
+
console.log("[Agent] Received invoke request:", {
|
|
1539
1471
|
threadId,
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
console.
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1472
|
+
message: message.substring(0, 50),
|
|
1473
|
+
modelId
|
|
1474
|
+
});
|
|
1475
|
+
if (!window) {
|
|
1476
|
+
console.error("[Agent] No window found");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const existingController = activeRuns.get(threadId);
|
|
1480
|
+
if (existingController) {
|
|
1481
|
+
console.log("[Agent] Aborting existing stream for thread:", threadId);
|
|
1482
|
+
existingController.abort();
|
|
1483
|
+
activeRuns.delete(threadId);
|
|
1484
|
+
}
|
|
1485
|
+
const abortController = new AbortController();
|
|
1486
|
+
activeRuns.set(threadId, abortController);
|
|
1487
|
+
const onWindowClosed = () => {
|
|
1488
|
+
console.log("[Agent] Window closed, aborting stream for thread:", threadId);
|
|
1489
|
+
abortController.abort();
|
|
1490
|
+
};
|
|
1491
|
+
window.once("closed", onWindowClosed);
|
|
1492
|
+
try {
|
|
1549
1493
|
const thread = getThread(threadId);
|
|
1550
1494
|
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1495
|
+
console.log("[Agent] Thread metadata:", metadata);
|
|
1551
1496
|
const workspacePath = metadata.workspacePath;
|
|
1552
1497
|
if (!workspacePath) {
|
|
1553
1498
|
window.webContents.send(channel, {
|
|
1554
1499
|
type: "error",
|
|
1555
|
-
error: "
|
|
1500
|
+
error: "WORKSPACE_REQUIRED",
|
|
1501
|
+
message: "Please select a workspace folder before sending messages."
|
|
1556
1502
|
});
|
|
1557
1503
|
return;
|
|
1558
1504
|
}
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const abortController = new AbortController();
|
|
1565
|
-
activeRuns.set(threadId, abortController);
|
|
1566
|
-
try {
|
|
1567
|
-
const agent = await createAgentRuntime({ threadId, workspacePath });
|
|
1568
|
-
const config = {
|
|
1505
|
+
const agent = await createAgentRuntime({ threadId, workspacePath, modelId });
|
|
1506
|
+
const humanMessage = new messages.HumanMessage(message);
|
|
1507
|
+
const stream = await agent.stream(
|
|
1508
|
+
{ messages: [humanMessage] },
|
|
1509
|
+
{
|
|
1569
1510
|
configurable: { thread_id: threadId },
|
|
1570
1511
|
signal: abortController.signal,
|
|
1571
1512
|
streamMode: ["messages", "values"],
|
|
1572
1513
|
recursionLimit: 1e3
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1516
|
+
for await (const chunk of stream) {
|
|
1517
|
+
if (abortController.signal.aborted) break;
|
|
1518
|
+
const [mode, data] = chunk;
|
|
1519
|
+
window.webContents.send(channel, {
|
|
1520
|
+
type: "stream",
|
|
1521
|
+
mode,
|
|
1522
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
if (!abortController.signal.aborted) {
|
|
1526
|
+
window.webContents.send(channel, { type: "done" });
|
|
1527
|
+
}
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1530
|
+
if (!isAbortError) {
|
|
1531
|
+
console.error("[Agent] Error:", error);
|
|
1532
|
+
window.webContents.send(channel, {
|
|
1533
|
+
type: "error",
|
|
1534
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
} finally {
|
|
1538
|
+
window.removeListener("closed", onWindowClosed);
|
|
1539
|
+
activeRuns.delete(threadId);
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
ipcMain.on("agent:resume", async (event, { threadId, command, modelId }) => {
|
|
1543
|
+
const channel = `agent:stream:${threadId}`;
|
|
1544
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1545
|
+
console.log("[Agent] Received resume request:", { threadId, command, modelId });
|
|
1546
|
+
if (!window) {
|
|
1547
|
+
console.error("[Agent] No window found for resume");
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const thread = getThread(threadId);
|
|
1551
|
+
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1552
|
+
const workspacePath = metadata.workspacePath;
|
|
1553
|
+
if (!workspacePath) {
|
|
1554
|
+
window.webContents.send(channel, {
|
|
1555
|
+
type: "error",
|
|
1556
|
+
error: "Workspace path is required"
|
|
1557
|
+
});
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const existingController = activeRuns.get(threadId);
|
|
1561
|
+
if (existingController) {
|
|
1562
|
+
existingController.abort();
|
|
1563
|
+
activeRuns.delete(threadId);
|
|
1564
|
+
}
|
|
1565
|
+
const abortController = new AbortController();
|
|
1566
|
+
activeRuns.set(threadId, abortController);
|
|
1567
|
+
try {
|
|
1568
|
+
const agent = await createAgentRuntime({ threadId, workspacePath, modelId });
|
|
1569
|
+
const config = {
|
|
1570
|
+
configurable: { thread_id: threadId },
|
|
1571
|
+
signal: abortController.signal,
|
|
1572
|
+
streamMode: ["messages", "values"],
|
|
1573
|
+
recursionLimit: 1e3
|
|
1574
|
+
};
|
|
1575
|
+
const decisionType = command?.resume?.decision || "approve";
|
|
1576
|
+
const resumeValue = { decisions: [{ type: decisionType }] };
|
|
1577
|
+
const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
|
|
1578
|
+
for await (const chunk of stream) {
|
|
1579
|
+
if (abortController.signal.aborted) break;
|
|
1580
|
+
const [mode, data] = chunk;
|
|
1581
|
+
window.webContents.send(channel, {
|
|
1582
|
+
type: "stream",
|
|
1583
|
+
mode,
|
|
1584
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (!abortController.signal.aborted) {
|
|
1588
|
+
window.webContents.send(channel, { type: "done" });
|
|
1589
|
+
}
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1592
|
+
if (!isAbortError) {
|
|
1593
|
+
console.error("[Agent] Resume error:", error);
|
|
1594
|
+
window.webContents.send(channel, {
|
|
1595
|
+
type: "error",
|
|
1596
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
} finally {
|
|
1600
|
+
activeRuns.delete(threadId);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
ipcMain.on("agent:interrupt", async (event, { threadId, decision }) => {
|
|
1604
|
+
const channel = `agent:stream:${threadId}`;
|
|
1605
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1606
|
+
if (!window) {
|
|
1607
|
+
console.error("[Agent] No window found for interrupt response");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const thread = getThread(threadId);
|
|
1611
|
+
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1612
|
+
const workspacePath = metadata.workspacePath;
|
|
1613
|
+
const modelId = metadata.model;
|
|
1614
|
+
if (!workspacePath) {
|
|
1615
|
+
window.webContents.send(channel, {
|
|
1616
|
+
type: "error",
|
|
1617
|
+
error: "Workspace path is required"
|
|
1618
|
+
});
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const existingController = activeRuns.get(threadId);
|
|
1622
|
+
if (existingController) {
|
|
1623
|
+
existingController.abort();
|
|
1624
|
+
activeRuns.delete(threadId);
|
|
1625
|
+
}
|
|
1626
|
+
const abortController = new AbortController();
|
|
1627
|
+
activeRuns.set(threadId, abortController);
|
|
1628
|
+
try {
|
|
1629
|
+
const agent = await createAgentRuntime({ threadId, workspacePath, modelId });
|
|
1630
|
+
const config = {
|
|
1631
|
+
configurable: { thread_id: threadId },
|
|
1632
|
+
signal: abortController.signal,
|
|
1633
|
+
streamMode: ["messages", "values"],
|
|
1634
|
+
recursionLimit: 1e3
|
|
1635
|
+
};
|
|
1636
|
+
if (decision.type === "approve") {
|
|
1637
|
+
const stream = await agent.stream(null, config);
|
|
1577
1638
|
for await (const chunk of stream) {
|
|
1578
1639
|
if (abortController.signal.aborted) break;
|
|
1579
1640
|
const [mode, data] = chunk;
|
|
@@ -1586,85 +1647,22 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1586
1647
|
if (!abortController.signal.aborted) {
|
|
1587
1648
|
window.webContents.send(channel, { type: "done" });
|
|
1588
1649
|
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (!isAbortError) {
|
|
1592
|
-
console.error("[Agent] Resume error:", error);
|
|
1593
|
-
window.webContents.send(channel, {
|
|
1594
|
-
type: "error",
|
|
1595
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
} finally {
|
|
1599
|
-
activeRuns.delete(threadId);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
);
|
|
1603
|
-
ipcMain.on(
|
|
1604
|
-
"agent:interrupt",
|
|
1605
|
-
async (event, { threadId, decision }) => {
|
|
1606
|
-
const channel = `agent:stream:${threadId}`;
|
|
1607
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1608
|
-
if (!window) {
|
|
1609
|
-
console.error("[Agent] No window found for interrupt response");
|
|
1610
|
-
return;
|
|
1650
|
+
} else if (decision.type === "reject") {
|
|
1651
|
+
window.webContents.send(channel, { type: "done" });
|
|
1611
1652
|
}
|
|
1612
|
-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1655
|
+
if (!isAbortError) {
|
|
1656
|
+
console.error("[Agent] Interrupt error:", error);
|
|
1616
1657
|
window.webContents.send(channel, {
|
|
1617
1658
|
type: "error",
|
|
1618
|
-
error:
|
|
1659
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1619
1660
|
});
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
const existingController = activeRuns.get(threadId);
|
|
1623
|
-
if (existingController) {
|
|
1624
|
-
existingController.abort();
|
|
1625
|
-
activeRuns.delete(threadId);
|
|
1626
|
-
}
|
|
1627
|
-
const abortController = new AbortController();
|
|
1628
|
-
activeRuns.set(threadId, abortController);
|
|
1629
|
-
try {
|
|
1630
|
-
const agent = await createAgentRuntime({ threadId, workspacePath });
|
|
1631
|
-
const config = {
|
|
1632
|
-
configurable: { thread_id: threadId },
|
|
1633
|
-
signal: abortController.signal,
|
|
1634
|
-
streamMode: ["messages", "values"],
|
|
1635
|
-
recursionLimit: 1e3
|
|
1636
|
-
};
|
|
1637
|
-
if (decision.type === "approve") {
|
|
1638
|
-
const stream = await agent.stream(null, config);
|
|
1639
|
-
for await (const chunk of stream) {
|
|
1640
|
-
if (abortController.signal.aborted) break;
|
|
1641
|
-
const [mode, data] = chunk;
|
|
1642
|
-
window.webContents.send(channel, {
|
|
1643
|
-
type: "stream",
|
|
1644
|
-
mode,
|
|
1645
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
if (!abortController.signal.aborted) {
|
|
1649
|
-
window.webContents.send(channel, { type: "done" });
|
|
1650
|
-
}
|
|
1651
|
-
} else if (decision.type === "reject") {
|
|
1652
|
-
window.webContents.send(channel, { type: "done" });
|
|
1653
|
-
}
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1656
|
-
if (!isAbortError) {
|
|
1657
|
-
console.error("[Agent] Interrupt error:", error);
|
|
1658
|
-
window.webContents.send(channel, {
|
|
1659
|
-
type: "error",
|
|
1660
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
} finally {
|
|
1664
|
-
activeRuns.delete(threadId);
|
|
1665
1661
|
}
|
|
1662
|
+
} finally {
|
|
1663
|
+
activeRuns.delete(threadId);
|
|
1666
1664
|
}
|
|
1667
|
-
);
|
|
1665
|
+
});
|
|
1668
1666
|
ipcMain.handle("agent:cancel", async (_event, { threadId }) => {
|
|
1669
1667
|
const controller = activeRuns.get(threadId);
|
|
1670
1668
|
if (controller) {
|
|
@@ -1735,28 +1733,25 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1735
1733
|
title
|
|
1736
1734
|
};
|
|
1737
1735
|
});
|
|
1738
|
-
ipcMain.handle(
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
);
|
|
1736
|
+
ipcMain.handle("threads:update", async (_event, { threadId, updates }) => {
|
|
1737
|
+
const updateData = {};
|
|
1738
|
+
if (updates.title !== void 0) updateData.title = updates.title;
|
|
1739
|
+
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1740
|
+
if (updates.metadata !== void 0) updateData.metadata = JSON.stringify(updates.metadata);
|
|
1741
|
+
if (updates.thread_values !== void 0)
|
|
1742
|
+
updateData.thread_values = JSON.stringify(updates.thread_values);
|
|
1743
|
+
const row = updateThread(threadId, updateData);
|
|
1744
|
+
if (!row) throw new Error("Thread not found");
|
|
1745
|
+
return {
|
|
1746
|
+
thread_id: row.thread_id,
|
|
1747
|
+
created_at: new Date(row.created_at),
|
|
1748
|
+
updated_at: new Date(row.updated_at),
|
|
1749
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1750
|
+
status: row.status,
|
|
1751
|
+
thread_values: row.thread_values ? JSON.parse(row.thread_values) : void 0,
|
|
1752
|
+
title: row.title
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1760
1755
|
ipcMain.handle("threads:delete", async (_event, threadId) => {
|
|
1761
1756
|
console.log("[Threads] Deleting thread:", threadId);
|
|
1762
1757
|
deleteThread(threadId);
|
|
@@ -1792,27 +1787,6 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1792
1787
|
return generateTitle(message);
|
|
1793
1788
|
});
|
|
1794
1789
|
}
|
|
1795
|
-
const originalConsoleError = console.error;
|
|
1796
|
-
console.error = (...args) => {
|
|
1797
|
-
const message = args.map((a) => String(a)).join(" ");
|
|
1798
|
-
if (message.includes("Controller is already closed") || message.includes("ERR_INVALID_STATE") || message.includes("StreamMessagesHandler") && message.includes("aborted")) {
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
originalConsoleError.apply(console, args);
|
|
1802
|
-
};
|
|
1803
|
-
process.on("uncaughtException", (error) => {
|
|
1804
|
-
if (error.message?.includes("Controller is already closed") || error.message?.includes("aborted")) {
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
originalConsoleError("Uncaught exception:", error);
|
|
1808
|
-
});
|
|
1809
|
-
process.on("unhandledRejection", (reason) => {
|
|
1810
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
1811
|
-
if (message?.includes("Controller is already closed") || message?.includes("aborted")) {
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
originalConsoleError("Unhandled rejection:", reason);
|
|
1815
|
-
});
|
|
1816
1790
|
let mainWindow = null;
|
|
1817
1791
|
const isDev = !electron.app.isPackaged;
|
|
1818
1792
|
function createWindow() {
|
package/out/preload/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const electronAPI = {
|
|
|
20
20
|
const api = {
|
|
21
21
|
agent: {
|
|
22
22
|
// Send message and receive events via callback
|
|
23
|
-
invoke: (threadId, message, onEvent) => {
|
|
23
|
+
invoke: (threadId, message, onEvent, modelId) => {
|
|
24
24
|
const channel = `agent:stream:${threadId}`;
|
|
25
25
|
const handler = (_, data) => {
|
|
26
26
|
onEvent(data);
|
|
@@ -29,13 +29,13 @@ const api = {
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
electron.ipcRenderer.on(channel, handler);
|
|
32
|
-
electron.ipcRenderer.send("agent:invoke", { threadId, message });
|
|
32
|
+
electron.ipcRenderer.send("agent:invoke", { threadId, message, modelId });
|
|
33
33
|
return () => {
|
|
34
34
|
electron.ipcRenderer.removeListener(channel, handler);
|
|
35
35
|
};
|
|
36
36
|
},
|
|
37
37
|
// Stream agent events for useStream transport
|
|
38
|
-
streamAgent: (threadId, message, command, onEvent) => {
|
|
38
|
+
streamAgent: (threadId, message, command, onEvent, modelId) => {
|
|
39
39
|
const channel = `agent:stream:${threadId}`;
|
|
40
40
|
const handler = (_, data) => {
|
|
41
41
|
onEvent(data);
|
|
@@ -45,9 +45,9 @@ const api = {
|
|
|
45
45
|
};
|
|
46
46
|
electron.ipcRenderer.on(channel, handler);
|
|
47
47
|
if (command) {
|
|
48
|
-
electron.ipcRenderer.send("agent:resume", { threadId, command });
|
|
48
|
+
electron.ipcRenderer.send("agent:resume", { threadId, command, modelId });
|
|
49
49
|
} else {
|
|
50
|
-
electron.ipcRenderer.send("agent:invoke", { threadId, message });
|
|
50
|
+
electron.ipcRenderer.send("agent:invoke", { threadId, message, modelId });
|
|
51
51
|
}
|
|
52
52
|
return () => {
|
|
53
53
|
electron.ipcRenderer.removeListener(channel, handler);
|