@uniqueli/openwork 0.2.1 → 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 +13 -6
- package/bin/cli.js +15 -14
- package/out/main/index.js +228 -269
- package/out/preload/index.js +12 -9
- package/out/renderer/assets/{index-BPV5Z3ZG.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
|
});
|
|
@@ -1367,9 +1357,10 @@ function getModelInstance(modelId) {
|
|
|
1367
1357
|
configuration: {
|
|
1368
1358
|
baseURL: matchingConfig.baseUrl,
|
|
1369
1359
|
defaultHeaders: {
|
|
1370
|
-
|
|
1360
|
+
Authorization: `Bearer ${cleanApiKey}`
|
|
1371
1361
|
}
|
|
1372
1362
|
},
|
|
1363
|
+
temperature: 0.3,
|
|
1373
1364
|
timeout: 6e4,
|
|
1374
1365
|
maxRetries: 2
|
|
1375
1366
|
});
|
|
@@ -1388,7 +1379,8 @@ function getModelInstance(modelId) {
|
|
|
1388
1379
|
}
|
|
1389
1380
|
return new anthropic.ChatAnthropic({
|
|
1390
1381
|
model,
|
|
1391
|
-
anthropicApiKey: apiKey
|
|
1382
|
+
anthropicApiKey: apiKey,
|
|
1383
|
+
temperature: 0.3
|
|
1392
1384
|
});
|
|
1393
1385
|
} else if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4")) {
|
|
1394
1386
|
const apiKey = getApiKey("openai");
|
|
@@ -1398,7 +1390,8 @@ function getModelInstance(modelId) {
|
|
|
1398
1390
|
}
|
|
1399
1391
|
return new openai.ChatOpenAI({
|
|
1400
1392
|
model,
|
|
1401
|
-
openAIApiKey: apiKey
|
|
1393
|
+
openAIApiKey: apiKey,
|
|
1394
|
+
temperature: 0.3
|
|
1402
1395
|
});
|
|
1403
1396
|
} else if (model.startsWith("gemini")) {
|
|
1404
1397
|
const apiKey = getApiKey("google");
|
|
@@ -1408,7 +1401,8 @@ function getModelInstance(modelId) {
|
|
|
1408
1401
|
}
|
|
1409
1402
|
return new googleGenai.ChatGoogleGenerativeAI({
|
|
1410
1403
|
model,
|
|
1411
|
-
apiKey
|
|
1404
|
+
apiKey,
|
|
1405
|
+
temperature: 0.3
|
|
1412
1406
|
});
|
|
1413
1407
|
}
|
|
1414
1408
|
return model;
|
|
@@ -1630,100 +1624,32 @@ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
|
|
|
1630
1624
|
const activeRuns = /* @__PURE__ */ new Map();
|
|
1631
1625
|
function registerAgentHandlers(ipcMain) {
|
|
1632
1626
|
console.log("[Agent] Registering agent handlers...");
|
|
1633
|
-
ipcMain.on(
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1638
|
-
console.log("[Agent] Received invoke request:", {
|
|
1639
|
-
threadId,
|
|
1640
|
-
message: message.substring(0, 50)
|
|
1641
|
-
});
|
|
1642
|
-
if (!window) {
|
|
1643
|
-
console.error("[Agent] No window found");
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
const existingController = activeRuns.get(threadId);
|
|
1647
|
-
if (existingController) {
|
|
1648
|
-
console.log("[Agent] Aborting existing stream for thread:", threadId);
|
|
1649
|
-
existingController.abort();
|
|
1650
|
-
activeRuns.delete(threadId);
|
|
1651
|
-
}
|
|
1652
|
-
const abortController = new AbortController();
|
|
1653
|
-
activeRuns.set(threadId, abortController);
|
|
1654
|
-
const onWindowClosed = () => {
|
|
1655
|
-
console.log("[Agent] Window closed, aborting stream for thread:", threadId);
|
|
1656
|
-
abortController.abort();
|
|
1657
|
-
};
|
|
1658
|
-
window.once("closed", onWindowClosed);
|
|
1659
|
-
try {
|
|
1660
|
-
const thread = getThread(threadId);
|
|
1661
|
-
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1662
|
-
const workspacePath = metadata.workspacePath;
|
|
1663
|
-
const currentModel = metadata.currentModel;
|
|
1664
|
-
if (!workspacePath) {
|
|
1665
|
-
window.webContents.send(channel, {
|
|
1666
|
-
type: "error",
|
|
1667
|
-
error: "WORKSPACE_REQUIRED",
|
|
1668
|
-
message: "Please select a workspace folder before sending messages."
|
|
1669
|
-
});
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
const agent = await createAgentRuntime({
|
|
1673
|
-
threadId,
|
|
1674
|
-
workspacePath,
|
|
1675
|
-
modelId: currentModel
|
|
1676
|
-
});
|
|
1677
|
-
const humanMessage = new messages.HumanMessage(message);
|
|
1678
|
-
const stream = await agent.stream(
|
|
1679
|
-
{ messages: [humanMessage] },
|
|
1680
|
-
{
|
|
1681
|
-
configurable: { thread_id: threadId },
|
|
1682
|
-
signal: abortController.signal,
|
|
1683
|
-
streamMode: ["messages", "values"],
|
|
1684
|
-
recursionLimit: 1e3
|
|
1685
|
-
}
|
|
1686
|
-
);
|
|
1687
|
-
for await (const chunk of stream) {
|
|
1688
|
-
if (abortController.signal.aborted) break;
|
|
1689
|
-
const [mode, data] = chunk;
|
|
1690
|
-
window.webContents.send(channel, {
|
|
1691
|
-
type: "stream",
|
|
1692
|
-
mode,
|
|
1693
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
if (!abortController.signal.aborted) {
|
|
1697
|
-
window.webContents.send(channel, { type: "done" });
|
|
1698
|
-
}
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1701
|
-
if (!isAbortError) {
|
|
1702
|
-
console.error("[Agent] Error:", error);
|
|
1703
|
-
window.webContents.send(channel, {
|
|
1704
|
-
type: "error",
|
|
1705
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
} finally {
|
|
1709
|
-
window.removeListener("closed", onWindowClosed);
|
|
1710
|
-
activeRuns.delete(threadId);
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
);
|
|
1714
|
-
ipcMain.on(
|
|
1715
|
-
"agent:resume",
|
|
1716
|
-
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:", {
|
|
1717
1631
|
threadId,
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
console.
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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 {
|
|
1727
1653
|
const thread = getThread(threadId);
|
|
1728
1654
|
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1729
1655
|
const workspacePath = metadata.workspacePath;
|
|
@@ -1731,32 +1657,157 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1731
1657
|
if (!workspacePath) {
|
|
1732
1658
|
window.webContents.send(channel, {
|
|
1733
1659
|
type: "error",
|
|
1734
|
-
error: "
|
|
1660
|
+
error: "WORKSPACE_REQUIRED",
|
|
1661
|
+
message: "Please select a workspace folder before sending messages."
|
|
1735
1662
|
});
|
|
1736
1663
|
return;
|
|
1737
1664
|
}
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
threadId,
|
|
1748
|
-
workspacePath,
|
|
1749
|
-
modelId: currentModel
|
|
1750
|
-
});
|
|
1751
|
-
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
|
+
{
|
|
1752
1674
|
configurable: { thread_id: threadId },
|
|
1753
1675
|
signal: abortController.signal,
|
|
1754
1676
|
streamMode: ["messages", "values"],
|
|
1755
1677
|
recursionLimit: 1e3
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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);
|
|
1760
1811
|
for await (const chunk of stream) {
|
|
1761
1812
|
if (abortController.signal.aborted) break;
|
|
1762
1813
|
const [mode, data] = chunk;
|
|
@@ -1769,90 +1820,22 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1769
1820
|
if (!abortController.signal.aborted) {
|
|
1770
1821
|
window.webContents.send(channel, { type: "done" });
|
|
1771
1822
|
}
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
if (!isAbortError) {
|
|
1775
|
-
console.error("[Agent] Resume error:", error);
|
|
1776
|
-
window.webContents.send(channel, {
|
|
1777
|
-
type: "error",
|
|
1778
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
} finally {
|
|
1782
|
-
activeRuns.delete(threadId);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
);
|
|
1786
|
-
ipcMain.on(
|
|
1787
|
-
"agent:interrupt",
|
|
1788
|
-
async (event, { threadId, decision }) => {
|
|
1789
|
-
const channel = `agent:stream:${threadId}`;
|
|
1790
|
-
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1791
|
-
if (!window) {
|
|
1792
|
-
console.error("[Agent] No window found for interrupt response");
|
|
1793
|
-
return;
|
|
1823
|
+
} else if (decision.type === "reject") {
|
|
1824
|
+
window.webContents.send(channel, { type: "done" });
|
|
1794
1825
|
}
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
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);
|
|
1800
1830
|
window.webContents.send(channel, {
|
|
1801
1831
|
type: "error",
|
|
1802
|
-
error:
|
|
1832
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1803
1833
|
});
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
const existingController = activeRuns.get(threadId);
|
|
1807
|
-
if (existingController) {
|
|
1808
|
-
existingController.abort();
|
|
1809
|
-
activeRuns.delete(threadId);
|
|
1810
|
-
}
|
|
1811
|
-
const abortController = new AbortController();
|
|
1812
|
-
activeRuns.set(threadId, abortController);
|
|
1813
|
-
try {
|
|
1814
|
-
const agent = await createAgentRuntime({
|
|
1815
|
-
threadId,
|
|
1816
|
-
workspacePath,
|
|
1817
|
-
modelId: currentModel
|
|
1818
|
-
});
|
|
1819
|
-
const config = {
|
|
1820
|
-
configurable: { thread_id: threadId },
|
|
1821
|
-
signal: abortController.signal,
|
|
1822
|
-
streamMode: ["messages", "values"],
|
|
1823
|
-
recursionLimit: 1e3
|
|
1824
|
-
};
|
|
1825
|
-
if (decision.type === "approve") {
|
|
1826
|
-
const stream = await agent.stream(null, config);
|
|
1827
|
-
for await (const chunk of stream) {
|
|
1828
|
-
if (abortController.signal.aborted) break;
|
|
1829
|
-
const [mode, data] = chunk;
|
|
1830
|
-
window.webContents.send(channel, {
|
|
1831
|
-
type: "stream",
|
|
1832
|
-
mode,
|
|
1833
|
-
data: JSON.parse(JSON.stringify(data))
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
if (!abortController.signal.aborted) {
|
|
1837
|
-
window.webContents.send(channel, { type: "done" });
|
|
1838
|
-
}
|
|
1839
|
-
} else if (decision.type === "reject") {
|
|
1840
|
-
window.webContents.send(channel, { type: "done" });
|
|
1841
|
-
}
|
|
1842
|
-
} catch (error) {
|
|
1843
|
-
const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
|
|
1844
|
-
if (!isAbortError) {
|
|
1845
|
-
console.error("[Agent] Interrupt error:", error);
|
|
1846
|
-
window.webContents.send(channel, {
|
|
1847
|
-
type: "error",
|
|
1848
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
} finally {
|
|
1852
|
-
activeRuns.delete(threadId);
|
|
1853
1834
|
}
|
|
1835
|
+
} finally {
|
|
1836
|
+
activeRuns.delete(threadId);
|
|
1854
1837
|
}
|
|
1855
|
-
);
|
|
1838
|
+
});
|
|
1856
1839
|
ipcMain.handle("agent:cancel", async (_event, { threadId }) => {
|
|
1857
1840
|
const controller = activeRuns.get(threadId);
|
|
1858
1841
|
if (controller) {
|
|
@@ -1923,28 +1906,25 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1923
1906
|
title
|
|
1924
1907
|
};
|
|
1925
1908
|
});
|
|
1926
|
-
ipcMain.handle(
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
};
|
|
1946
|
-
}
|
|
1947
|
-
);
|
|
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
|
+
});
|
|
1948
1928
|
ipcMain.handle("threads:delete", async (_event, threadId) => {
|
|
1949
1929
|
console.log("[Threads] Deleting thread:", threadId);
|
|
1950
1930
|
deleteThread(threadId);
|
|
@@ -1980,27 +1960,6 @@ function registerThreadHandlers(ipcMain) {
|
|
|
1980
1960
|
return generateTitle(message);
|
|
1981
1961
|
});
|
|
1982
1962
|
}
|
|
1983
|
-
const originalConsoleError = console.error;
|
|
1984
|
-
console.error = (...args) => {
|
|
1985
|
-
const message = args.map((a) => String(a)).join(" ");
|
|
1986
|
-
if (message.includes("Controller is already closed") || message.includes("ERR_INVALID_STATE") || message.includes("StreamMessagesHandler") && message.includes("aborted")) {
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
originalConsoleError.apply(console, args);
|
|
1990
|
-
};
|
|
1991
|
-
process.on("uncaughtException", (error) => {
|
|
1992
|
-
if (error.message?.includes("Controller is already closed") || error.message?.includes("aborted")) {
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
originalConsoleError("Uncaught exception:", error);
|
|
1996
|
-
});
|
|
1997
|
-
process.on("unhandledRejection", (reason) => {
|
|
1998
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
1999
|
-
if (message?.includes("Controller is already closed") || message?.includes("aborted")) {
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
originalConsoleError("Unhandled rejection:", reason);
|
|
2003
|
-
});
|
|
2004
1963
|
let mainWindow = null;
|
|
2005
1964
|
const isDev = !electron.app.isPackaged;
|
|
2006
1965
|
function createWindow() {
|