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 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('child_process')
8
- const path = require('path')
7
+ const { spawn } = require("child_process")
8
+ const path = require("path")
9
9
 
10
10
  // Set process title for Activity Monitor
11
- process.title = 'openwork'
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('--version') || args.includes('-v')) {
17
- const { version } = require('../package.json')
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('--help') || args.includes('-h')) {
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('electron')
36
+ const electron = require("electron")
37
37
 
38
38
  // Launch electron with our main process
39
- const mainPath = path.join(__dirname, '..', 'out', 'main', 'index.js')
39
+ const mainPath = path.join(__dirname, "..", "out", "main", "index.js")
40
40
 
41
41
  const child = spawn(electron, [mainPath, ...args], {
42
- stdio: 'inherit'
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('SIGINT', () => forwardSignal('SIGINT'))
53
- process.on('SIGTERM', () => forwardSignal('SIGTERM'))
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('close', (code) => {
57
+ child.on("close", (code) => {
57
58
  process.exit(code ?? 0)
58
59
  })
59
60
 
60
- child.on('error', (err) => {
61
- console.error('Failed to start openwork:', err.message)
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(([_, v]) => v).map(([k, v]) => `${k}=${v}`);
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
- "models:setApiKey",
379
- async (_event, { provider, apiKey }) => {
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
- "agent:invoke",
1462
- async (event, { threadId, message }) => {
1463
- const channel = `agent:stream:${threadId}`;
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
- command
1541
- }) => {
1542
- const channel = `agent:stream:${threadId}`;
1543
- const window = electron.BrowserWindow.fromWebContents(event.sender);
1544
- console.log("[Agent] Received resume request:", { threadId, command });
1545
- if (!window) {
1546
- console.error("[Agent] No window found for resume");
1547
- return;
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: "Workspace path is required"
1500
+ error: "WORKSPACE_REQUIRED",
1501
+ message: "Please select a workspace folder before sending messages."
1556
1502
  });
1557
1503
  return;
1558
1504
  }
1559
- const existingController = activeRuns.get(threadId);
1560
- if (existingController) {
1561
- existingController.abort();
1562
- activeRuns.delete(threadId);
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
- const decisionType = command?.resume?.decision || "approve";
1575
- const resumeValue = { decisions: [{ type: decisionType }] };
1576
- const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
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
- } catch (error) {
1590
- const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
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
- const thread = getThread(threadId);
1613
- const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
1614
- const workspacePath = metadata.workspacePath;
1615
- if (!workspacePath) {
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: "Workspace path is required"
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
- "threads:update",
1740
- async (_event, { threadId, updates }) => {
1741
- const updateData = {};
1742
- if (updates.title !== void 0) updateData.title = updates.title;
1743
- if (updates.status !== void 0) updateData.status = updates.status;
1744
- if (updates.metadata !== void 0)
1745
- updateData.metadata = JSON.stringify(updates.metadata);
1746
- if (updates.thread_values !== void 0) updateData.thread_values = JSON.stringify(updates.thread_values);
1747
- const row = updateThread(threadId, updateData);
1748
- if (!row) throw new Error("Thread not found");
1749
- return {
1750
- thread_id: row.thread_id,
1751
- created_at: new Date(row.created_at),
1752
- updated_at: new Date(row.updated_at),
1753
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1754
- status: row.status,
1755
- thread_values: row.thread_values ? JSON.parse(row.thread_values) : void 0,
1756
- title: row.title
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() {
@@ -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);