@uniqueli/openwork 0.2.0 → 0.2.2
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 +18 -6
- package/bin/cli.js +15 -14
- package/out/main/index.js +232 -269
- package/out/preload/index.js +12 -9
- package/out/renderer/assets/{index-wQN5U7g5.js → index-BayYTupF.js} +1001 -608
- package/out/renderer/assets/{index-BtAM3QNQ.css → index-iDdc8OMS.css} +86 -17
- package/out/renderer/index.html +2 -2
- package/package.json +8 -7
- package/resources/README.md +0 -16
package/out/main/index.js
CHANGED
|
@@ -107,7 +107,9 @@ const ENV_VAR_NAMES = {
|
|
|
107
107
|
anthropic: "ANTHROPIC_API_KEY",
|
|
108
108
|
openai: "OPENAI_API_KEY",
|
|
109
109
|
google: "GOOGLE_API_KEY",
|
|
110
|
-
|
|
110
|
+
ollama: ""
|
|
111
|
+
// Ollama doesn't require an API key
|
|
112
|
+
// Custom providers have their own env var pattern
|
|
111
113
|
};
|
|
112
114
|
function getOpenworkDir() {
|
|
113
115
|
if (!fs.existsSync(OPENWORK_DIR)) {
|
|
@@ -156,7 +158,7 @@ function parseEnvFile() {
|
|
|
156
158
|
}
|
|
157
159
|
function writeEnvFile(env) {
|
|
158
160
|
getOpenworkDir();
|
|
159
|
-
const lines = Object.entries(env).filter((
|
|
161
|
+
const lines = Object.entries(env).filter((entry) => entry[1]).map(([k, v]) => `${k}=${v}`);
|
|
160
162
|
fs.writeFileSync(getEnvFilePath(), lines.join("\n") + "\n");
|
|
161
163
|
}
|
|
162
164
|
function getApiKey(provider) {
|
|
@@ -271,8 +273,7 @@ const store = new Store({
|
|
|
271
273
|
const PROVIDERS = [
|
|
272
274
|
{ id: "anthropic", name: "Anthropic" },
|
|
273
275
|
{ id: "openai", name: "OpenAI" },
|
|
274
|
-
{ id: "google", name: "Google" }
|
|
275
|
-
{ id: "custom", name: "Custom API" }
|
|
276
|
+
{ id: "google", name: "Google" }
|
|
276
277
|
];
|
|
277
278
|
const AVAILABLE_MODELS = [
|
|
278
279
|
// Anthropic Claude 4.5 series (latest as of Jan 2026)
|
|
@@ -417,6 +418,14 @@ const AVAILABLE_MODELS = [
|
|
|
417
418
|
description: "State-of-the-art reasoning and multimodal understanding",
|
|
418
419
|
available: true
|
|
419
420
|
},
|
|
421
|
+
{
|
|
422
|
+
id: "gemini-3-flash-preview",
|
|
423
|
+
name: "Gemini 3 Flash Preview",
|
|
424
|
+
provider: "google",
|
|
425
|
+
model: "gemini-3-flash-preview",
|
|
426
|
+
description: "Fast frontier-class model with low latency and cost",
|
|
427
|
+
available: true
|
|
428
|
+
},
|
|
420
429
|
{
|
|
421
430
|
id: "gemini-2.5-pro",
|
|
422
431
|
name: "Gemini 2.5 Pro",
|
|
@@ -440,41 +449,27 @@ const AVAILABLE_MODELS = [
|
|
|
440
449
|
model: "gemini-2.5-flash-lite",
|
|
441
450
|
description: "Fast, low-cost, high-performance model",
|
|
442
451
|
available: true
|
|
443
|
-
},
|
|
444
|
-
// Custom API
|
|
445
|
-
{
|
|
446
|
-
id: "custom",
|
|
447
|
-
name: "Custom API",
|
|
448
|
-
provider: "custom",
|
|
449
|
-
model: "custom",
|
|
450
|
-
description: "Use your own OpenAI-compatible API endpoint",
|
|
451
|
-
available: true
|
|
452
452
|
}
|
|
453
453
|
];
|
|
454
454
|
function registerModelHandlers(ipcMain) {
|
|
455
455
|
ipcMain.handle("models:list", async () => {
|
|
456
456
|
const customConfigs = getCustomApiConfigs();
|
|
457
|
-
const models = AVAILABLE_MODELS.
|
|
457
|
+
const models = AVAILABLE_MODELS.map((model) => ({
|
|
458
|
+
...model,
|
|
459
|
+
available: hasApiKey(model.provider)
|
|
460
|
+
}));
|
|
458
461
|
for (const config of customConfigs) {
|
|
459
462
|
const modelId = config.model || `custom-${config.id}`;
|
|
460
463
|
models.push({
|
|
461
464
|
id: modelId,
|
|
462
465
|
name: config.model || config.name,
|
|
463
|
-
// Display the model name or config name
|
|
464
466
|
provider: config.id,
|
|
465
|
-
// Use config ID as provider ID (dynamic)
|
|
466
467
|
model: modelId,
|
|
467
468
|
description: `${config.name} - ${config.baseUrl}`,
|
|
468
469
|
available: true
|
|
469
470
|
});
|
|
470
471
|
}
|
|
471
|
-
return models
|
|
472
|
-
const isCustom = customConfigs.some((c) => c.id === model.provider);
|
|
473
|
-
return {
|
|
474
|
-
...model,
|
|
475
|
-
available: isCustom ? true : hasApiKey(model.provider)
|
|
476
|
-
};
|
|
477
|
-
});
|
|
472
|
+
return models;
|
|
478
473
|
});
|
|
479
474
|
ipcMain.handle("models:getDefault", async () => {
|
|
480
475
|
return store.get("defaultModel", "claude-sonnet-4-5-20250929");
|
|
@@ -482,12 +477,9 @@ function registerModelHandlers(ipcMain) {
|
|
|
482
477
|
ipcMain.handle("models:setDefault", async (_event, modelId) => {
|
|
483
478
|
store.set("defaultModel", modelId);
|
|
484
479
|
});
|
|
485
|
-
ipcMain.handle(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
setApiKey(provider, apiKey);
|
|
489
|
-
}
|
|
490
|
-
);
|
|
480
|
+
ipcMain.handle("models:setApiKey", async (_event, { provider, apiKey }) => {
|
|
481
|
+
setApiKey(provider, apiKey);
|
|
482
|
+
});
|
|
491
483
|
ipcMain.handle("models:getApiKey", async (_event, provider) => {
|
|
492
484
|
return getApiKey(provider) ?? null;
|
|
493
485
|
});
|
|
@@ -495,17 +487,15 @@ function registerModelHandlers(ipcMain) {
|
|
|
495
487
|
deleteApiKey(provider);
|
|
496
488
|
});
|
|
497
489
|
ipcMain.handle("models:listProviders", async () => {
|
|
498
|
-
const standardProviders = PROVIDERS.
|
|
490
|
+
const standardProviders = PROVIDERS.map((provider) => ({
|
|
499
491
|
...provider,
|
|
500
492
|
hasApiKey: hasApiKey(provider.id)
|
|
501
493
|
}));
|
|
502
494
|
const customConfigs = getCustomApiConfigs();
|
|
503
495
|
const customProviders = customConfigs.map((config) => ({
|
|
504
496
|
id: config.id,
|
|
505
|
-
// Dynamic provider ID
|
|
506
497
|
name: config.name,
|
|
507
498
|
hasApiKey: true
|
|
508
|
-
// Custom configs always have their API key
|
|
509
499
|
}));
|
|
510
500
|
return [...standardProviders, ...customProviders];
|
|
511
501
|
});
|
|
@@ -1356,6 +1346,10 @@ function getModelInstance(modelId) {
|
|
|
1356
1346
|
cleanApiKeyLength: cleanApiKey?.length,
|
|
1357
1347
|
apiKeyPrefix: cleanApiKey?.substring(0, 10)
|
|
1358
1348
|
});
|
|
1349
|
+
if (cleanApiKey) {
|
|
1350
|
+
process.env.OPENAI_API_KEY = cleanApiKey;
|
|
1351
|
+
console.log("[Runtime] Set OPENAI_API_KEY environment variable for deepagents compatibility");
|
|
1352
|
+
}
|
|
1359
1353
|
try {
|
|
1360
1354
|
const chatModel = new openai.ChatOpenAI({
|
|
1361
1355
|
model: matchingConfig.model || model,
|
|
@@ -1363,9 +1357,10 @@ function getModelInstance(modelId) {
|
|
|
1363
1357
|
configuration: {
|
|
1364
1358
|
baseURL: matchingConfig.baseUrl,
|
|
1365
1359
|
defaultHeaders: {
|
|
1366
|
-
|
|
1360
|
+
Authorization: `Bearer ${cleanApiKey}`
|
|
1367
1361
|
}
|
|
1368
1362
|
},
|
|
1363
|
+
temperature: 0.3,
|
|
1369
1364
|
timeout: 6e4,
|
|
1370
1365
|
maxRetries: 2
|
|
1371
1366
|
});
|
|
@@ -1384,7 +1379,8 @@ function getModelInstance(modelId) {
|
|
|
1384
1379
|
}
|
|
1385
1380
|
return new anthropic.ChatAnthropic({
|
|
1386
1381
|
model,
|
|
1387
|
-
anthropicApiKey: apiKey
|
|
1382
|
+
anthropicApiKey: apiKey,
|
|
1383
|
+
temperature: 0.3
|
|
1388
1384
|
});
|
|
1389
1385
|
} else if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4")) {
|
|
1390
1386
|
const apiKey = getApiKey("openai");
|
|
@@ -1394,7 +1390,8 @@ function getModelInstance(modelId) {
|
|
|
1394
1390
|
}
|
|
1395
1391
|
return new openai.ChatOpenAI({
|
|
1396
1392
|
model,
|
|
1397
|
-
openAIApiKey: apiKey
|
|
1393
|
+
openAIApiKey: apiKey,
|
|
1394
|
+
temperature: 0.3
|
|
1398
1395
|
});
|
|
1399
1396
|
} else if (model.startsWith("gemini")) {
|
|
1400
1397
|
const apiKey = getApiKey("google");
|
|
@@ -1404,7 +1401,8 @@ function getModelInstance(modelId) {
|
|
|
1404
1401
|
}
|
|
1405
1402
|
return new googleGenai.ChatGoogleGenerativeAI({
|
|
1406
1403
|
model,
|
|
1407
|
-
apiKey
|
|
1404
|
+
apiKey,
|
|
1405
|
+
temperature: 0.3
|
|
1408
1406
|
});
|
|
1409
1407
|
}
|
|
1410
1408
|
return model;
|
|
@@ -1626,100 +1624,32 @@ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
|
|
|
1626
1624
|
const activeRuns = /* @__PURE__ */ new Map();
|
|
1627
1625
|
function registerAgentHandlers(ipcMain) {
|
|
1628
1626
|
console.log("[Agent] Registering agent handlers...");
|
|
1629
|
-
ipcMain.on(
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1634
|
-
console.log("[Agent] Received invoke request:", {
|
|
1635
|
-
threadId,
|
|
1636
|
-
message: message.substring(0, 50)
|
|
1637
|
-
});
|
|
1638
|
-
if (!window) {
|
|
1639
|
-
console.error("[Agent] No window found");
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
const existingController = activeRuns.get(threadId);
|
|
1643
|
-
if (existingController) {
|
|
1644
|
-
console.log("[Agent] Aborting existing stream for thread:", threadId);
|
|
1645
|
-
existingController.abort();
|
|
1646
|
-
activeRuns.delete(threadId);
|
|
1647
|
-
}
|
|
1648
|
-
const abortController = new AbortController();
|
|
1649
|
-
activeRuns.set(threadId, abortController);
|
|
1650
|
-
const onWindowClosed = () => {
|
|
1651
|
-
console.log("[Agent] Window closed, aborting stream for thread:", threadId);
|
|
1652
|
-
abortController.abort();
|
|
1653
|
-
};
|
|
1654
|
-
window.once("closed", onWindowClosed);
|
|
1655
|
-
try {
|
|
1656
|
-
const thread = getThread(threadId);
|
|
1657
|
-
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1658
|
-
const workspacePath = metadata.workspacePath;
|
|
1659
|
-
const currentModel = metadata.currentModel;
|
|
1660
|
-
if (!workspacePath) {
|
|
1661
|
-
window.webContents.send(channel, {
|
|
1662
|
-
type: "error",
|
|
1663
|
-
error: "WORKSPACE_REQUIRED",
|
|
1664
|
-
message: "Please select a workspace folder before sending messages."
|
|
1665
|
-
});
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
const agent = await createAgentRuntime({
|
|
1669
|
-
threadId,
|
|
1670
|
-
workspacePath,
|
|
1671
|
-
modelId: currentModel
|
|
1672
|
-
});
|
|
1673
|
-
const humanMessage = new messages.HumanMessage(message);
|
|
1674
|
-
const stream = await agent.stream(
|
|
1675
|
-
{ messages: [humanMessage] },
|
|
1676
|
-
{
|
|
1677
|
-
configurable: { thread_id: threadId },
|
|
1678
|
-
signal: abortController.signal,
|
|
1679
|
-
streamMode: ["messages", "values"],
|
|
1680
|
-
recursionLimit: 1e3
|
|
1681
|
-
}
|
|
1682
|
-
);
|
|
1683
|
-
for await (const chunk of stream) {
|
|
1684
|
-
if (abortController.signal.aborted) break;
|
|
1685
|
-
const [mode, data] = chunk;
|
|
1686
|
-
window.webContents.send(channel, {
|
|
1687
|
-
type: "stream",
|
|
1688
|
-
mode,
|
|
1689
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
if (!abortController.signal.aborted) {
|
|
1693
|
-
window.webContents.send(channel, { type: "done" });
|
|
1694
|
-
}
|
|
1695
|
-
} catch (error) {
|
|
1696
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1697
|
-
if (!isAbortError) {
|
|
1698
|
-
console.error("[Agent] Error:", error);
|
|
1699
|
-
window.webContents.send(channel, {
|
|
1700
|
-
type: "error",
|
|
1701
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
} finally {
|
|
1705
|
-
window.removeListener("closed", onWindowClosed);
|
|
1706
|
-
activeRuns.delete(threadId);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
);
|
|
1710
|
-
ipcMain.on(
|
|
1711
|
-
"agent:resume",
|
|
1712
|
-
async (event, {
|
|
1627
|
+
ipcMain.on("agent:invoke", async (event, { threadId, message, modelId }) => {
|
|
1628
|
+
const channel = `agent:stream:${threadId}`;
|
|
1629
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1630
|
+
console.log("[Agent] Received invoke request:", {
|
|
1713
1631
|
threadId,
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
console.
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1632
|
+
message: message.substring(0, 50),
|
|
1633
|
+
modelId
|
|
1634
|
+
});
|
|
1635
|
+
if (!window) {
|
|
1636
|
+
console.error("[Agent] No window found");
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const existingController = activeRuns.get(threadId);
|
|
1640
|
+
if (existingController) {
|
|
1641
|
+
console.log("[Agent] Aborting existing stream for thread:", threadId);
|
|
1642
|
+
existingController.abort();
|
|
1643
|
+
activeRuns.delete(threadId);
|
|
1644
|
+
}
|
|
1645
|
+
const abortController = new AbortController();
|
|
1646
|
+
activeRuns.set(threadId, abortController);
|
|
1647
|
+
const onWindowClosed = () => {
|
|
1648
|
+
console.log("[Agent] Window closed, aborting stream for thread:", threadId);
|
|
1649
|
+
abortController.abort();
|
|
1650
|
+
};
|
|
1651
|
+
window.once("closed", onWindowClosed);
|
|
1652
|
+
try {
|
|
1723
1653
|
const thread = getThread(threadId);
|
|
1724
1654
|
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1725
1655
|
const workspacePath = metadata.workspacePath;
|
|
@@ -1727,32 +1657,157 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1727
1657
|
if (!workspacePath) {
|
|
1728
1658
|
window.webContents.send(channel, {
|
|
1729
1659
|
type: "error",
|
|
1730
|
-
error: "
|
|
1660
|
+
error: "WORKSPACE_REQUIRED",
|
|
1661
|
+
message: "Please select a workspace folder before sending messages."
|
|
1731
1662
|
});
|
|
1732
1663
|
return;
|
|
1733
1664
|
}
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
}
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
threadId,
|
|
1744
|
-
workspacePath,
|
|
1745
|
-
modelId: currentModel
|
|
1746
|
-
});
|
|
1747
|
-
const config = {
|
|
1665
|
+
const agent = await createAgentRuntime({
|
|
1666
|
+
threadId,
|
|
1667
|
+
workspacePath,
|
|
1668
|
+
modelId: currentModel || modelId
|
|
1669
|
+
});
|
|
1670
|
+
const humanMessage = new messages.HumanMessage(message);
|
|
1671
|
+
const stream = await agent.stream(
|
|
1672
|
+
{ messages: [humanMessage] },
|
|
1673
|
+
{
|
|
1748
1674
|
configurable: { thread_id: threadId },
|
|
1749
1675
|
signal: abortController.signal,
|
|
1750
1676
|
streamMode: ["messages", "values"],
|
|
1751
1677
|
recursionLimit: 1e3
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
for await (const chunk of stream) {
|
|
1681
|
+
if (abortController.signal.aborted) break;
|
|
1682
|
+
const [mode, data] = chunk;
|
|
1683
|
+
window.webContents.send(channel, {
|
|
1684
|
+
type: "stream",
|
|
1685
|
+
mode,
|
|
1686
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
if (!abortController.signal.aborted) {
|
|
1690
|
+
window.webContents.send(channel, { type: "done" });
|
|
1691
|
+
}
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1694
|
+
if (!isAbortError) {
|
|
1695
|
+
console.error("[Agent] Error:", error);
|
|
1696
|
+
window.webContents.send(channel, {
|
|
1697
|
+
type: "error",
|
|
1698
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
} finally {
|
|
1702
|
+
window.removeListener("closed", onWindowClosed);
|
|
1703
|
+
activeRuns.delete(threadId);
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
ipcMain.on("agent:resume", async (event, { threadId, command, modelId }) => {
|
|
1707
|
+
const channel = `agent:stream:${threadId}`;
|
|
1708
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1709
|
+
console.log("[Agent] Received resume request:", { threadId, command, modelId });
|
|
1710
|
+
if (!window) {
|
|
1711
|
+
console.error("[Agent] No window found for resume");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const thread = getThread(threadId);
|
|
1715
|
+
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1716
|
+
const workspacePath = metadata.workspacePath;
|
|
1717
|
+
const currentModel = metadata.currentModel;
|
|
1718
|
+
if (!workspacePath) {
|
|
1719
|
+
window.webContents.send(channel, {
|
|
1720
|
+
type: "error",
|
|
1721
|
+
error: "Workspace path is required"
|
|
1722
|
+
});
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const existingController = activeRuns.get(threadId);
|
|
1726
|
+
if (existingController) {
|
|
1727
|
+
existingController.abort();
|
|
1728
|
+
activeRuns.delete(threadId);
|
|
1729
|
+
}
|
|
1730
|
+
const abortController = new AbortController();
|
|
1731
|
+
activeRuns.set(threadId, abortController);
|
|
1732
|
+
try {
|
|
1733
|
+
const agent = await createAgentRuntime({
|
|
1734
|
+
threadId,
|
|
1735
|
+
workspacePath,
|
|
1736
|
+
modelId: currentModel || modelId
|
|
1737
|
+
});
|
|
1738
|
+
const config = {
|
|
1739
|
+
configurable: { thread_id: threadId },
|
|
1740
|
+
signal: abortController.signal,
|
|
1741
|
+
streamMode: ["messages", "values"],
|
|
1742
|
+
recursionLimit: 1e3
|
|
1743
|
+
};
|
|
1744
|
+
const decisionType = command?.resume?.decision || "approve";
|
|
1745
|
+
const resumeValue = { decisions: [{ type: decisionType }] };
|
|
1746
|
+
const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
|
|
1747
|
+
for await (const chunk of stream) {
|
|
1748
|
+
if (abortController.signal.aborted) break;
|
|
1749
|
+
const [mode, data] = chunk;
|
|
1750
|
+
window.webContents.send(channel, {
|
|
1751
|
+
type: "stream",
|
|
1752
|
+
mode,
|
|
1753
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
if (!abortController.signal.aborted) {
|
|
1757
|
+
window.webContents.send(channel, { type: "done" });
|
|
1758
|
+
}
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1761
|
+
if (!isAbortError) {
|
|
1762
|
+
console.error("[Agent] Resume error:", error);
|
|
1763
|
+
window.webContents.send(channel, {
|
|
1764
|
+
type: "error",
|
|
1765
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
} finally {
|
|
1769
|
+
activeRuns.delete(threadId);
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
ipcMain.on("agent:interrupt", async (event, { threadId, decision }) => {
|
|
1773
|
+
const channel = `agent:stream:${threadId}`;
|
|
1774
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1775
|
+
if (!window) {
|
|
1776
|
+
console.error("[Agent] No window found for interrupt response");
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const thread = getThread(threadId);
|
|
1780
|
+
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1781
|
+
const workspacePath = metadata.workspacePath;
|
|
1782
|
+
const currentModel = metadata.currentModel;
|
|
1783
|
+
if (!workspacePath) {
|
|
1784
|
+
window.webContents.send(channel, {
|
|
1785
|
+
type: "error",
|
|
1786
|
+
error: "Workspace path is required"
|
|
1787
|
+
});
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const existingController = activeRuns.get(threadId);
|
|
1791
|
+
if (existingController) {
|
|
1792
|
+
existingController.abort();
|
|
1793
|
+
activeRuns.delete(threadId);
|
|
1794
|
+
}
|
|
1795
|
+
const abortController = new AbortController();
|
|
1796
|
+
activeRuns.set(threadId, abortController);
|
|
1797
|
+
try {
|
|
1798
|
+
const agent = await createAgentRuntime({
|
|
1799
|
+
threadId,
|
|
1800
|
+
workspacePath,
|
|
1801
|
+
modelId: currentModel
|
|
1802
|
+
});
|
|
1803
|
+
const config = {
|
|
1804
|
+
configurable: { thread_id: threadId },
|
|
1805
|
+
signal: abortController.signal,
|
|
1806
|
+
streamMode: ["messages", "values"],
|
|
1807
|
+
recursionLimit: 1e3
|
|
1808
|
+
};
|
|
1809
|
+
if (decision.type === "approve") {
|
|
1810
|
+
const stream = await agent.stream(null, config);
|
|
1756
1811
|
for await (const chunk of stream) {
|
|
1757
1812
|
if (abortController.signal.aborted) break;
|
|
1758
1813
|
const [mode, data] = chunk;
|
|
@@ -1765,90 +1820,22 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1765
1820
|
if (!abortController.signal.aborted) {
|
|
1766
1821
|
window.webContents.send(channel, { type: "done" });
|
|
1767
1822
|
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
if (!isAbortError) {
|
|
1771
|
-
console.error("[Agent] Resume error:", error);
|
|
1772
|
-
window.webContents.send(channel, {
|
|
1773
|
-
type: "error",
|
|
1774
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
} finally {
|
|
1778
|
-
activeRuns.delete(threadId);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
);
|
|
1782
|
-
ipcMain.on(
|
|
1783
|
-
"agent:interrupt",
|
|
1784
|
-
async (event, { threadId, decision }) => {
|
|
1785
|
-
const channel = `agent:stream:${threadId}`;
|
|
1786
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1787
|
-
if (!window) {
|
|
1788
|
-
console.error("[Agent] No window found for interrupt response");
|
|
1789
|
-
return;
|
|
1823
|
+
} else if (decision.type === "reject") {
|
|
1824
|
+
window.webContents.send(channel, { type: "done" });
|
|
1790
1825
|
}
|
|
1791
|
-
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if (!workspacePath) {
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1828
|
+
if (!isAbortError) {
|
|
1829
|
+
console.error("[Agent] Interrupt error:", error);
|
|
1796
1830
|
window.webContents.send(channel, {
|
|
1797
1831
|
type: "error",
|
|
1798
|
-
error:
|
|
1799
|
-
});
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
const existingController = activeRuns.get(threadId);
|
|
1803
|
-
if (existingController) {
|
|
1804
|
-
existingController.abort();
|
|
1805
|
-
activeRuns.delete(threadId);
|
|
1806
|
-
}
|
|
1807
|
-
const abortController = new AbortController();
|
|
1808
|
-
activeRuns.set(threadId, abortController);
|
|
1809
|
-
try {
|
|
1810
|
-
const agent = await createAgentRuntime({
|
|
1811
|
-
threadId,
|
|
1812
|
-
workspacePath,
|
|
1813
|
-
modelId: currentModel
|
|
1832
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1814
1833
|
});
|
|
1815
|
-
const config = {
|
|
1816
|
-
configurable: { thread_id: threadId },
|
|
1817
|
-
signal: abortController.signal,
|
|
1818
|
-
streamMode: ["messages", "values"],
|
|
1819
|
-
recursionLimit: 1e3
|
|
1820
|
-
};
|
|
1821
|
-
if (decision.type === "approve") {
|
|
1822
|
-
const stream = await agent.stream(null, config);
|
|
1823
|
-
for await (const chunk of stream) {
|
|
1824
|
-
if (abortController.signal.aborted) break;
|
|
1825
|
-
const [mode, data] = chunk;
|
|
1826
|
-
window.webContents.send(channel, {
|
|
1827
|
-
type: "stream",
|
|
1828
|
-
mode,
|
|
1829
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
if (!abortController.signal.aborted) {
|
|
1833
|
-
window.webContents.send(channel, { type: "done" });
|
|
1834
|
-
}
|
|
1835
|
-
} else if (decision.type === "reject") {
|
|
1836
|
-
window.webContents.send(channel, { type: "done" });
|
|
1837
|
-
}
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1840
|
-
if (!isAbortError) {
|
|
1841
|
-
console.error("[Agent] Interrupt error:", error);
|
|
1842
|
-
window.webContents.send(channel, {
|
|
1843
|
-
type: "error",
|
|
1844
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
} finally {
|
|
1848
|
-
activeRuns.delete(threadId);
|
|
1849
1834
|
}
|
|
1835
|
+
} finally {
|
|
1836
|
+
activeRuns.delete(threadId);
|
|
1850
1837
|
}
|
|
1851
|
-
);
|
|
1838
|
+
});
|
|
1852
1839
|
ipcMain.handle("agent:cancel", async (_event, { threadId }) => {
|
|
1853
1840
|
const controller = activeRuns.get(threadId);
|
|
1854
1841
|
if (controller) {
|
|
@@ -1919,28 +1906,25 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1919
1906
|
title
|
|
1920
1907
|
};
|
|
1921
1908
|
});
|
|
1922
|
-
ipcMain.handle(
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
);
|
|
1909
|
+
ipcMain.handle("threads:update", async (_event, { threadId, updates }) => {
|
|
1910
|
+
const updateData = {};
|
|
1911
|
+
if (updates.title !== void 0) updateData.title = updates.title;
|
|
1912
|
+
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1913
|
+
if (updates.metadata !== void 0) updateData.metadata = JSON.stringify(updates.metadata);
|
|
1914
|
+
if (updates.thread_values !== void 0)
|
|
1915
|
+
updateData.thread_values = JSON.stringify(updates.thread_values);
|
|
1916
|
+
const row = updateThread(threadId, updateData);
|
|
1917
|
+
if (!row) throw new Error("Thread not found");
|
|
1918
|
+
return {
|
|
1919
|
+
thread_id: row.thread_id,
|
|
1920
|
+
created_at: new Date(row.created_at),
|
|
1921
|
+
updated_at: new Date(row.updated_at),
|
|
1922
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1923
|
+
status: row.status,
|
|
1924
|
+
thread_values: row.thread_values ? JSON.parse(row.thread_values) : void 0,
|
|
1925
|
+
title: row.title
|
|
1926
|
+
};
|
|
1927
|
+
});
|
|
1944
1928
|
ipcMain.handle("threads:delete", async (_event, threadId) => {
|
|
1945
1929
|
console.log("[Threads] Deleting thread:", threadId);
|
|
1946
1930
|
deleteThread(threadId);
|
|
@@ -1976,27 +1960,6 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1976
1960
|
return generateTitle(message);
|
|
1977
1961
|
});
|
|
1978
1962
|
}
|
|
1979
|
-
const originalConsoleError = console.error;
|
|
1980
|
-
console.error = (...args) => {
|
|
1981
|
-
const message = args.map((a) => String(a)).join(" ");
|
|
1982
|
-
if (message.includes("Controller is already closed") || message.includes("ERR_INVALID_STATE") || message.includes("StreamMessagesHandler") && message.includes("aborted")) {
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
originalConsoleError.apply(console, args);
|
|
1986
|
-
};
|
|
1987
|
-
process.on("uncaughtException", (error) => {
|
|
1988
|
-
if (error.message?.includes("Controller is already closed") || error.message?.includes("aborted")) {
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
originalConsoleError("Uncaught exception:", error);
|
|
1992
|
-
});
|
|
1993
|
-
process.on("unhandledRejection", (reason) => {
|
|
1994
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
1995
|
-
if (message?.includes("Controller is already closed") || message?.includes("aborted")) {
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
originalConsoleError("Unhandled rejection:", reason);
|
|
1999
|
-
});
|
|
2000
1963
|
let mainWindow = null;
|
|
2001
1964
|
const isDev = !electron.app.isPackaged;
|
|
2002
1965
|
function createWindow() {
|