@nubase/terminal 0.0.0-stage → 0.1.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/bin/nubase.js +11 -0
- package/out/main/index.js +3218 -0
- package/out/preload/index.cjs +23 -0
- package/out/renderer/assets/BinaryPreviews-Dvmgctgl.js +262 -0
- package/out/renderer/assets/CodeEditor-BKx61p9c.js +2034 -0
- package/out/renderer/assets/DocumentPreviews-BA1gL-ZJ.js +66 -0
- package/out/renderer/assets/DocxPreview-CiY-SNbh.js +6955 -0
- package/out/renderer/assets/ImagePreview-DpnKfbxj.js +120 -0
- package/out/renderer/assets/PdfPreview-6oeuYaqY.js +24399 -0
- package/out/renderer/assets/PdfPreview-BT-DZbCt.css +6483 -0
- package/out/renderer/assets/SpreadsheetPreviews-B4Qby88A.js +23987 -0
- package/out/renderer/assets/abap-Cel38df4.js +1317 -0
- package/out/renderer/assets/apex-DzOmV_zF.js +355 -0
- package/out/renderer/assets/azcli-Xvapx-7p.js +56 -0
- package/out/renderer/assets/bat-BGMSIlWY.js +126 -0
- package/out/renderer/assets/bicep-CcpV5qxk.js +228 -0
- package/out/renderer/assets/cameligo-BfMDXVEx.js +233 -0
- package/out/renderer/assets/clojure-DFgIAJGu.js +801 -0
- package/out/renderer/assets/codicon-DF1abBS2.ttf +0 -0
- package/out/renderer/assets/coffee-Db4q5aZ8.js +308 -0
- package/out/renderer/assets/cpp-CEvPJv6f.js +450 -0
- package/out/renderer/assets/csharp-CVoBr97X.js +405 -0
- package/out/renderer/assets/csp-Cgzd5gxo.js +49 -0
- package/out/renderer/assets/css-D3M-RRVi.js +287 -0
- package/out/renderer/assets/css.worker-AXSlELAq.js +50031 -0
- package/out/renderer/assets/cssMode-CrMQfSUN.js +101 -0
- package/out/renderer/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
- package/out/renderer/assets/cypher-DJJgZ_3Q.js +326 -0
- package/out/renderer/assets/dart-Bkg7I-Qe.js +341 -0
- package/out/renderer/assets/dockerfile-B1r36HyK.js +194 -0
- package/out/renderer/assets/ecl-C1Ji6tZG.js +531 -0
- package/out/renderer/assets/editor-CXfYqUZ6.css +4816 -0
- package/out/renderer/assets/editor.api-zF37fz_e.js +127071 -0
- package/out/renderer/assets/elixir-D_-n8lGJ.js +526 -0
- package/out/renderer/assets/fileTypes-nBt-Y6NI.js +56306 -0
- package/out/renderer/assets/flow9-CCdkJPNN.js +179 -0
- package/out/renderer/assets/freemarker2-CBlvIP9I.js +733 -0
- package/out/renderer/assets/fsharp-D5KoOpm3.js +248 -0
- package/out/renderer/assets/go-B7BFdMAL.js +264 -0
- package/out/renderer/assets/graphql-BqIezLde.js +177 -0
- package/out/renderer/assets/handlebars-CG4DAk5_.js +414 -0
- package/out/renderer/assets/hcl-C1GQCKC0.js +222 -0
- package/out/renderer/assets/html-C-_R4s2w.js +335 -0
- package/out/renderer/assets/html.worker-vQ8gO4kF.js +30584 -0
- package/out/renderer/assets/htmlMode-mGTQJ-lr.js +125 -0
- package/out/renderer/assets/index-CQHlaLTi.js +46080 -0
- package/out/renderer/assets/index-DBYJcRdn.css +3124 -0
- package/out/renderer/assets/ini-DDFxEGYN.js +97 -0
- package/out/renderer/assets/java-BqvgSBNj.js +272 -0
- package/out/renderer/assets/javascript-BuIet4p-.js +69 -0
- package/out/renderer/assets/json.worker-DBhrCMTN.js +23259 -0
- package/out/renderer/assets/jsonMode-OAArikPQ.js +769 -0
- package/out/renderer/assets/julia-D30HQT3I.js +688 -0
- package/out/renderer/assets/kotlin-Bql4-FwM.js +303 -0
- package/out/renderer/assets/less-D_rhcFqZ.js +245 -0
- package/out/renderer/assets/lexon-CjhVPeai.js +188 -0
- package/out/renderer/assets/liquid-DJNDWhYP.js +277 -0
- package/out/renderer/assets/lspLanguageFeatures-BIH0_3rO.css +24 -0
- package/out/renderer/assets/lspLanguageFeatures-Bvh4jxNk.js +2583 -0
- package/out/renderer/assets/lua-HQuy0vuU.js +203 -0
- package/out/renderer/assets/m3-CIJ7obE2.js +287 -0
- package/out/renderer/assets/markdown-I0GroHKS.js +265 -0
- package/out/renderer/assets/mdx-B4We-zdi.js +330 -0
- package/out/renderer/assets/mips-DY_Z1uin.js +190 -0
- package/out/renderer/assets/msdax-YkUW9rXl.js +416 -0
- package/out/renderer/assets/mysql-CIm9uV8f.js +921 -0
- package/out/renderer/assets/objective-c-JkhNEfNv.js +233 -0
- package/out/renderer/assets/pascal-Dvq-PxOC.js +289 -0
- package/out/renderer/assets/pascaligo-A9tZTvdV.js +204 -0
- package/out/renderer/assets/pdf_viewer-8SNFor7s.js +7639 -0
- package/out/renderer/assets/perl-C2Ujz2HB.js +720 -0
- package/out/renderer/assets/pgsql-BHB-cYm3.js +879 -0
- package/out/renderer/assets/php-DkNik5Vk.js +548 -0
- package/out/renderer/assets/pla-DQ-RrdDE.js +149 -0
- package/out/renderer/assets/postiats-f1cpw-r_.js +702 -0
- package/out/renderer/assets/powerquery-CwYp-rYn.js +940 -0
- package/out/renderer/assets/powershell-DNVb682q.js +252 -0
- package/out/renderer/assets/protobuf-C73bXEb8.js +663 -0
- package/out/renderer/assets/pug-BxvM69yY.js +419 -0
- package/out/renderer/assets/python-Hlw9kU7X.js +389 -0
- package/out/renderer/assets/qsharp-BCs2AvEL.js +335 -0
- package/out/renderer/assets/r-B7wnzr1V.js +282 -0
- package/out/renderer/assets/razor-BswxUzye.js +615 -0
- package/out/renderer/assets/redis-BQXcxMfX.js +324 -0
- package/out/renderer/assets/redshift-2dZdIUPH.js +838 -0
- package/out/renderer/assets/restructuredtext-CVT2zSWP.js +290 -0
- package/out/renderer/assets/ruby-DjmMI5Dp.js +612 -0
- package/out/renderer/assets/rust-BhSCZj0F.js +382 -0
- package/out/renderer/assets/sb-DaUIZery.js +146 -0
- package/out/renderer/assets/scala-CYIN7D7O.js +589 -0
- package/out/renderer/assets/scheme-DanUmhC2.js +173 -0
- package/out/renderer/assets/scss-BVILLTDt.js +420 -0
- package/out/renderer/assets/shell-Bpairu2a.js +293 -0
- package/out/renderer/assets/solidity-OLm4MSRm.js +1395 -0
- package/out/renderer/assets/sophia-DJz2Vk1M.js +234 -0
- package/out/renderer/assets/sparql-Bk5uRAQt.js +237 -0
- package/out/renderer/assets/sql-DQBN9zz5.js +858 -0
- package/out/renderer/assets/st-50mV4H0S.js +657 -0
- package/out/renderer/assets/swift-Yc0Q28XL.js +376 -0
- package/out/renderer/assets/systemverilog-Du7vdcmu.js +616 -0
- package/out/renderer/assets/tcl-DG7u3TBj.js +312 -0
- package/out/renderer/assets/toggleHighContrast-CKRp-vZ7.css +3691 -0
- package/out/renderer/assets/toggleHighContrast-NyS1pk1Z.js +51247 -0
- package/out/renderer/assets/twig-C49MsNA9.js +438 -0
- package/out/renderer/assets/typescript-F2yeVXIn.js +382 -0
- package/out/renderer/assets/typespec-DA1dz9gQ.js +245 -0
- package/out/renderer/assets/useAsync-Bi56CNnI.js +30 -0
- package/out/renderer/assets/vb-BDNIgfQk.js +447 -0
- package/out/renderer/assets/wgsl-j-0nJ_62.js +457 -0
- package/out/renderer/assets/xml-BfG5_Ah8.js +131 -0
- package/out/renderer/assets/yaml-7gp9FQUG.js +224 -0
- package/out/renderer/index.html +14 -0
- package/package.json +66 -4
- package/resources/skills/nubase-extension-authoring/SKILL.md +273 -0
- package/resources/templates/extension/package.json.template +13 -0
- package/resources/templates/extension/src/host.ts.template +12 -0
- package/resources/templates/extension/src/ui.tsx.template +26 -0
- package/resources/templates/extension/tsconfig.json.template +4 -0
- package/README.md +0 -3
|
@@ -0,0 +1,3218 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { BaseWindow, BrowserWindow, Menu, WebContentsView, app, dialog, ipcMain, nativeImage, net, protocol, safeStorage, shell } from "electron";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { createReadStream, existsSync, mkdirSync, statSync, watch } from "node:fs";
|
|
8
|
+
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { Readable } from "node:stream";
|
|
12
|
+
import * as sdkHost from "@nubase/sdk/host";
|
|
13
|
+
import { defineHostExtension } from "@nubase/sdk/host";
|
|
14
|
+
import yauzl from "yauzl";
|
|
15
|
+
import { DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
//#region src/common/fileUrl.ts
|
|
17
|
+
/** Streams local files to the renderer, so images, media and PDFs load by URL at any size. */
|
|
18
|
+
var FILE_PROTOCOL = "nubase-file";
|
|
19
|
+
var HOST = "local";
|
|
20
|
+
var fromFileUrl = (url) => {
|
|
21
|
+
const parsed = new URL(url);
|
|
22
|
+
if (parsed.protocol !== `nubase-file:` || parsed.hostname !== HOST) return;
|
|
23
|
+
const path = decodeURIComponent(parsed.pathname);
|
|
24
|
+
return /^\/[a-zA-Z]:\//.test(path) ? path.slice(1) : path;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/common/host-contract.ts
|
|
28
|
+
var INVOKE_CHANNEL = "nubase:invoke";
|
|
29
|
+
var EVENT_CHANNEL = "nubase:event";
|
|
30
|
+
var EXTENSION_PROTOCOL = "nubase-ext";
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/main/menu.ts
|
|
33
|
+
/**
|
|
34
|
+
* A minimal menu: keeps the OS edit shortcuts (copy/paste) working while
|
|
35
|
+
* leaving shortcuts like ⌘W to Nubase's own keybinding system.
|
|
36
|
+
*/
|
|
37
|
+
var installApplicationMenu = (actions) => {
|
|
38
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
|
39
|
+
...process.platform === "darwin" ? [{
|
|
40
|
+
label: app.name,
|
|
41
|
+
submenu: [
|
|
42
|
+
{ role: "about" },
|
|
43
|
+
{ type: "separator" },
|
|
44
|
+
{ role: "hide" },
|
|
45
|
+
{ role: "hideOthers" },
|
|
46
|
+
{ role: "unhide" },
|
|
47
|
+
{ type: "separator" },
|
|
48
|
+
{ role: "quit" }
|
|
49
|
+
]
|
|
50
|
+
}] : [],
|
|
51
|
+
{
|
|
52
|
+
label: "File",
|
|
53
|
+
submenu: [{
|
|
54
|
+
label: "Open Folder…",
|
|
55
|
+
click: () => actions.openFolder()
|
|
56
|
+
}]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Edit",
|
|
60
|
+
submenu: [
|
|
61
|
+
{ role: "undo" },
|
|
62
|
+
{ role: "redo" },
|
|
63
|
+
{ type: "separator" },
|
|
64
|
+
{ role: "cut" },
|
|
65
|
+
{ role: "copy" },
|
|
66
|
+
{ role: "paste" },
|
|
67
|
+
{ role: "selectAll" }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "View",
|
|
72
|
+
submenu: [
|
|
73
|
+
{ role: "toggleDevTools" },
|
|
74
|
+
{ type: "separator" },
|
|
75
|
+
{ role: "resetZoom" },
|
|
76
|
+
{ role: "zoomIn" },
|
|
77
|
+
{ role: "zoomOut" },
|
|
78
|
+
{ type: "separator" },
|
|
79
|
+
{ role: "togglefullscreen" }
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{ role: "windowMenu" }
|
|
83
|
+
]));
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/main/paths.ts
|
|
87
|
+
/** Override with NUBASE_HOME to run against a scratch directory. */
|
|
88
|
+
var nubaseHome = process.env.NUBASE_HOME ?? join(homedir(), ".nubase");
|
|
89
|
+
var HOME_WORKSPACE_ID = "home";
|
|
90
|
+
var paths = {
|
|
91
|
+
home: nubaseHome,
|
|
92
|
+
settings: join(nubaseHome, "settings.json"),
|
|
93
|
+
/** Open folder tabs, in order, and the active one. */
|
|
94
|
+
workspaces: join(nubaseHome, "workspaces.json"),
|
|
95
|
+
/** The layout before folder tabs existed; migrated into the `~` tab. */
|
|
96
|
+
legacyLayout: join(nubaseHome, "workspaces", "default", "layout.json"),
|
|
97
|
+
storage: join(nubaseHome, "storage"),
|
|
98
|
+
secrets: join(nubaseHome, "secrets.json"),
|
|
99
|
+
extensions: join(nubaseHome, "extensions"),
|
|
100
|
+
agentDir: join(nubaseHome, "agent"),
|
|
101
|
+
logs: join(nubaseHome, "logs")
|
|
102
|
+
};
|
|
103
|
+
var workspaceIdOf = (folder) => folder === homedir() ? HOME_WORKSPACE_ID : createHash("sha1").update(folder).digest("hex").slice(0, 12);
|
|
104
|
+
/** Per-machine state of one folder tab (layout, extension history). Never inside the folder itself. */
|
|
105
|
+
var workspacePaths = (id) => {
|
|
106
|
+
const dir = join(nubaseHome, "workspaces", id);
|
|
107
|
+
return {
|
|
108
|
+
dir,
|
|
109
|
+
layout: join(dir, "layout.json"),
|
|
110
|
+
extensionsGitDir: join(dir, "extensions.git")
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/** Shareable configuration a folder can carry in its own `.nubase`. */
|
|
114
|
+
var folderPaths = (folder) => {
|
|
115
|
+
const dir = join(folder, ".nubase");
|
|
116
|
+
return {
|
|
117
|
+
dir,
|
|
118
|
+
settings: join(dir, "settings.json"),
|
|
119
|
+
extensions: join(dir, "extensions")
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/main/services/extensions/sharedModules.ts
|
|
124
|
+
/**
|
|
125
|
+
* Modules provided by the host at runtime instead of being bundled into each
|
|
126
|
+
* extension. Sharing them guarantees a single React instance and a single
|
|
127
|
+
* set of registries. Keep in sync with the renderer's module table.
|
|
128
|
+
*/
|
|
129
|
+
var UI_SHARED_MODULES = [
|
|
130
|
+
"react",
|
|
131
|
+
"react-dom",
|
|
132
|
+
"react-dom/client",
|
|
133
|
+
"react/jsx-runtime",
|
|
134
|
+
"react/jsx-dev-runtime",
|
|
135
|
+
"@nubase/ui",
|
|
136
|
+
"@nubase/sdk",
|
|
137
|
+
"lucide-react"
|
|
138
|
+
];
|
|
139
|
+
var HOST_SHARED_MODULES = ["@nubase/sdk/host", "@nubase/sdk"];
|
|
140
|
+
var UI_MODULES_GLOBAL = "__nubase_modules";
|
|
141
|
+
var HOST_MODULES_GLOBAL = "__nubase_host_modules";
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/main/services/extensions/ExtensionBuilder.ts
|
|
144
|
+
var nodeRequire$1 = createRequire(import.meta.url);
|
|
145
|
+
/**
|
|
146
|
+
* Rewrites imports of shared modules to read them from a global the host
|
|
147
|
+
* fills in. Shims are CommonJS so esbuild's interop maps named imports onto
|
|
148
|
+
* properties, without having to know each module's export list.
|
|
149
|
+
*/
|
|
150
|
+
var sharedModulesPlugin = (modules, globalName) => ({
|
|
151
|
+
name: "nubase-shared-modules",
|
|
152
|
+
setup(build) {
|
|
153
|
+
const filter = new RegExp(`^(${modules.map((m) => m.replace(/[/.]/g, "\\$&")).join("|")})$`);
|
|
154
|
+
build.onResolve({ filter }, (args) => ({
|
|
155
|
+
path: args.path,
|
|
156
|
+
namespace: "nubase-shared"
|
|
157
|
+
}));
|
|
158
|
+
build.onLoad({
|
|
159
|
+
filter: /.*/,
|
|
160
|
+
namespace: "nubase-shared"
|
|
161
|
+
}, (args) => ({
|
|
162
|
+
contents: `module.exports = globalThis.${globalName}[${JSON.stringify(args.path)}];`,
|
|
163
|
+
loader: "js"
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
var toDiagnostics = (messages, source, root) => messages.map((m) => ({
|
|
168
|
+
source,
|
|
169
|
+
message: m.text,
|
|
170
|
+
file: m.location?.file ? relative(root, join(root, m.location.file)) : void 0,
|
|
171
|
+
line: m.location?.line,
|
|
172
|
+
column: m.location?.column
|
|
173
|
+
}));
|
|
174
|
+
/** Builds extensions from source: bundles (esbuild), styles (Tailwind) and typechecks (tsc). */
|
|
175
|
+
var ExtensionBuilder = class {
|
|
176
|
+
async build(dir, manifest) {
|
|
177
|
+
const esbuild = await import("esbuild");
|
|
178
|
+
const outputs = [];
|
|
179
|
+
const diagnostics = [];
|
|
180
|
+
const bundle = async (entry, options) => {
|
|
181
|
+
try {
|
|
182
|
+
const result = await esbuild.build({
|
|
183
|
+
absWorkingDir: dir,
|
|
184
|
+
entryPoints: [join(dir, entry)],
|
|
185
|
+
bundle: true,
|
|
186
|
+
write: false,
|
|
187
|
+
platform: options.platform,
|
|
188
|
+
format: options.format,
|
|
189
|
+
target: options.platform === "node" ? "node22" : "chrome130",
|
|
190
|
+
outfile: options.outfile,
|
|
191
|
+
jsx: "automatic",
|
|
192
|
+
sourcemap: "inline",
|
|
193
|
+
logLevel: "silent",
|
|
194
|
+
plugins: [options.plugin],
|
|
195
|
+
external: options.platform === "node" ? ["electron"] : [],
|
|
196
|
+
define: { "process.env.NODE_ENV": "\"production\"" }
|
|
197
|
+
});
|
|
198
|
+
outputs.push(...result.outputFiles);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const failure = error;
|
|
201
|
+
diagnostics.push(...failure.errors ? toDiagnostics(failure.errors, "esbuild", dir) : [{
|
|
202
|
+
source: "esbuild",
|
|
203
|
+
message: String(error)
|
|
204
|
+
}]);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
if (manifest.ui) await bundle(manifest.ui, {
|
|
208
|
+
platform: "browser",
|
|
209
|
+
format: "esm",
|
|
210
|
+
outfile: join(dir, "dist", "ui.js"),
|
|
211
|
+
plugin: sharedModulesPlugin(UI_SHARED_MODULES, UI_MODULES_GLOBAL)
|
|
212
|
+
});
|
|
213
|
+
if (manifest.host) await bundle(manifest.host, {
|
|
214
|
+
platform: "node",
|
|
215
|
+
format: "cjs",
|
|
216
|
+
outfile: join(dir, "dist", "host.cjs"),
|
|
217
|
+
plugin: sharedModulesPlugin(HOST_SHARED_MODULES, HOST_MODULES_GLOBAL)
|
|
218
|
+
});
|
|
219
|
+
if (diagnostics.length > 0) return {
|
|
220
|
+
ok: false,
|
|
221
|
+
diagnostics
|
|
222
|
+
};
|
|
223
|
+
diagnostics.push(...await this.typecheck(dir));
|
|
224
|
+
if (diagnostics.length > 0) return {
|
|
225
|
+
ok: false,
|
|
226
|
+
diagnostics
|
|
227
|
+
};
|
|
228
|
+
let css;
|
|
229
|
+
if (manifest.ui) {
|
|
230
|
+
try {
|
|
231
|
+
css = await this.buildStyles(join(dir, dirname(manifest.ui)));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
diagnostics: [{
|
|
236
|
+
source: "tailwind",
|
|
237
|
+
message: error.message
|
|
238
|
+
}]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (css) outputs.push({
|
|
242
|
+
path: join(dir, "dist", "ui.css"),
|
|
243
|
+
contents: css
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const hash = createHash("sha256");
|
|
247
|
+
await mkdir(join(dir, "dist"), { recursive: true });
|
|
248
|
+
for (const output of outputs) {
|
|
249
|
+
hash.update(output.contents);
|
|
250
|
+
await writeFile(output.path, output.contents);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
buildId: hash.digest("hex").slice(0, 12),
|
|
255
|
+
hasUi: Boolean(manifest.ui),
|
|
256
|
+
hasHost: Boolean(manifest.host),
|
|
257
|
+
hasCss: Boolean(css)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** Compiles only the Tailwind utilities the extension's sources use. */
|
|
261
|
+
async buildStyles(sourceDir) {
|
|
262
|
+
const { compile } = await import("@tailwindcss/node");
|
|
263
|
+
const { Scanner } = await import("@tailwindcss/oxide");
|
|
264
|
+
const compiler = await compile([
|
|
265
|
+
"@import \"tailwindcss/theme\" layer(theme);",
|
|
266
|
+
"@import \"tailwindcss/utilities\" layer(utilities);",
|
|
267
|
+
"@import \"tw-animate-css\";",
|
|
268
|
+
(await readFile(nodeRequire$1.resolve("@nubase/ui/theme.css"), "utf8")).split("\n").filter((line) => !line.startsWith("@import")).join("\n")
|
|
269
|
+
].join("\n"), {
|
|
270
|
+
base: dirname(nodeRequire$1.resolve("tailwindcss/package.json")),
|
|
271
|
+
onDependency: () => {}
|
|
272
|
+
});
|
|
273
|
+
const candidates = new Scanner({ sources: [{
|
|
274
|
+
base: sourceDir,
|
|
275
|
+
pattern: "**/*",
|
|
276
|
+
negated: false
|
|
277
|
+
}] }).scan();
|
|
278
|
+
return candidates.length > 0 ? compiler.build(candidates) : void 0;
|
|
279
|
+
}
|
|
280
|
+
async typecheck(dir) {
|
|
281
|
+
const configPath = join(dir, "tsconfig.json");
|
|
282
|
+
if (!existsSync(configPath)) return [];
|
|
283
|
+
const ts = (await import("typescript")).default;
|
|
284
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
285
|
+
if (config.error) return [{
|
|
286
|
+
source: "typescript",
|
|
287
|
+
message: ts.flattenDiagnosticMessageText(config.error.messageText, "\n")
|
|
288
|
+
}];
|
|
289
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, dir);
|
|
290
|
+
const program = ts.createProgram({
|
|
291
|
+
rootNames: parsed.fileNames,
|
|
292
|
+
options: {
|
|
293
|
+
...parsed.options,
|
|
294
|
+
noEmit: true
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return ts.getPreEmitDiagnostics(program).filter((d) => d.category === ts.DiagnosticCategory.Error).slice(0, 30).map((d) => {
|
|
298
|
+
const position = d.file && d.start !== void 0 ? d.file.getLineAndCharacterOfPosition(d.start) : void 0;
|
|
299
|
+
return {
|
|
300
|
+
source: "typescript",
|
|
301
|
+
message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
|
|
302
|
+
file: d.file ? relative(dir, d.file.fileName) : void 0,
|
|
303
|
+
line: position ? position.line + 1 : void 0,
|
|
304
|
+
column: position ? position.character + 1 : void 0
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
/** Absolute paths to the type declarations extensions compile against. */
|
|
310
|
+
var resolveTypePaths = () => {
|
|
311
|
+
const typesOf = (pkg, file) => {
|
|
312
|
+
try {
|
|
313
|
+
return [join(dirname(nodeRequire$1.resolve(`${pkg}/package.json`)), file)];
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
react: typesOf("@types/react", "index.d.ts"),
|
|
320
|
+
"react/jsx-runtime": typesOf("@types/react", "jsx-runtime.d.ts"),
|
|
321
|
+
"react-dom": typesOf("@types/react-dom", "index.d.ts"),
|
|
322
|
+
"react-dom/client": typesOf("@types/react-dom", "client.d.ts"),
|
|
323
|
+
"@nubase/ui": typesOf("@nubase/ui", "dist/index.d.ts"),
|
|
324
|
+
"@nubase/sdk": typesOf("@nubase/sdk", "dist/index.d.ts"),
|
|
325
|
+
"@nubase/sdk/host": typesOf("@nubase/sdk", "dist/host.d.ts"),
|
|
326
|
+
"lucide-react": typesOf("lucide-react", "dist/lucide-react.d.ts")
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
var resolveTypeRoots = () => {
|
|
330
|
+
try {
|
|
331
|
+
return [dirname(dirname(nodeRequire$1.resolve("@types/node/package.json")))];
|
|
332
|
+
} catch {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/main/services/extensions/manifest.ts
|
|
338
|
+
var ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
339
|
+
var ManifestError = class extends Error {};
|
|
340
|
+
/** Reads the `nubase` field of `<dir>/package.json`. */
|
|
341
|
+
var readManifest = async (dir) => {
|
|
342
|
+
let pkg;
|
|
343
|
+
try {
|
|
344
|
+
pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new ManifestError(`Cannot read package.json: ${error.message}`);
|
|
347
|
+
}
|
|
348
|
+
const nubase = pkg.nubase;
|
|
349
|
+
if (!nubase) throw new ManifestError("package.json has no \"nubase\" field");
|
|
350
|
+
if (!nubase.id || !ID_PATTERN.test(nubase.id)) throw new ManifestError(`"nubase.id" must be lowercase kebab-case (got ${JSON.stringify(nubase.id)})`);
|
|
351
|
+
if (!nubase.ui && !nubase.host) throw new ManifestError("\"nubase\" must declare a \"ui\" and/or \"host\" entry");
|
|
352
|
+
return {
|
|
353
|
+
id: nubase.id,
|
|
354
|
+
displayName: nubase.displayName ?? nubase.id,
|
|
355
|
+
description: nubase.description,
|
|
356
|
+
ui: nubase.ui,
|
|
357
|
+
host: nubase.host
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
var isValidExtensionId = (id) => ID_PATTERN.test(id);
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/main/services/extensions/ExtensionSource.ts
|
|
363
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
364
|
+
/**
|
|
365
|
+
* One extensions directory: discovers, builds, watches and versions the
|
|
366
|
+
* extensions in it. Builds happen once per directory no matter how many
|
|
367
|
+
* folder tabs use it; each tab's ExtensionHost activates the results.
|
|
368
|
+
*/
|
|
369
|
+
var ExtensionSource = class {
|
|
370
|
+
entries = /* @__PURE__ */ new Map();
|
|
371
|
+
listeners = /* @__PURE__ */ new Set();
|
|
372
|
+
watcher;
|
|
373
|
+
pendingChanges = /* @__PURE__ */ new Map();
|
|
374
|
+
started;
|
|
375
|
+
constructor(deps) {
|
|
376
|
+
this.deps = deps;
|
|
377
|
+
}
|
|
378
|
+
get key() {
|
|
379
|
+
return this.deps.key;
|
|
380
|
+
}
|
|
381
|
+
get kind() {
|
|
382
|
+
return this.deps.kind;
|
|
383
|
+
}
|
|
384
|
+
get dir() {
|
|
385
|
+
return this.deps.dir;
|
|
386
|
+
}
|
|
387
|
+
start() {
|
|
388
|
+
this.started ??= (async () => {
|
|
389
|
+
await this.deps.repo.ensure();
|
|
390
|
+
await this.discover();
|
|
391
|
+
this.watch();
|
|
392
|
+
})();
|
|
393
|
+
return this.started;
|
|
394
|
+
}
|
|
395
|
+
ids() {
|
|
396
|
+
return [...this.entries.keys()];
|
|
397
|
+
}
|
|
398
|
+
get(id) {
|
|
399
|
+
return this.entries.get(id);
|
|
400
|
+
}
|
|
401
|
+
/** True when the folder exists on disk, even if it was never built. */
|
|
402
|
+
contains(id) {
|
|
403
|
+
return this.entries.has(id) || existsSync(join(this.pathOf(id), "package.json"));
|
|
404
|
+
}
|
|
405
|
+
pathOf(id) {
|
|
406
|
+
return join(this.deps.dir, id);
|
|
407
|
+
}
|
|
408
|
+
toInfo(entry) {
|
|
409
|
+
return {
|
|
410
|
+
id: entry.id,
|
|
411
|
+
displayName: entry.displayName,
|
|
412
|
+
description: entry.description,
|
|
413
|
+
path: entry.dir,
|
|
414
|
+
builtin: false,
|
|
415
|
+
source: this.deps.kind,
|
|
416
|
+
status: entry.status,
|
|
417
|
+
diagnostics: entry.diagnostics,
|
|
418
|
+
buildId: entry.buildId,
|
|
419
|
+
uiBundleUrl: entry.uiBundleUrl,
|
|
420
|
+
uiStylesUrl: entry.uiStylesUrl
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** Called after every status change; `rebuild` waits for listeners to finish. */
|
|
424
|
+
onChanged(listener) {
|
|
425
|
+
this.listeners.add(listener);
|
|
426
|
+
return () => this.listeners.delete(listener);
|
|
427
|
+
}
|
|
428
|
+
async rebuild(id) {
|
|
429
|
+
let entry = this.entries.get(id);
|
|
430
|
+
if (!entry) {
|
|
431
|
+
await this.discover(id);
|
|
432
|
+
entry = this.entries.get(id);
|
|
433
|
+
if (!entry) throw new Error(`No extension "${id}" in ${this.deps.dir}`);
|
|
434
|
+
return entry.building ?? entry;
|
|
435
|
+
}
|
|
436
|
+
if (entry.building) {
|
|
437
|
+
entry.rebuildQueued = true;
|
|
438
|
+
return entry.building;
|
|
439
|
+
}
|
|
440
|
+
const current = entry;
|
|
441
|
+
current.building = this.doBuild(current).finally(() => {
|
|
442
|
+
current.building = void 0;
|
|
443
|
+
});
|
|
444
|
+
const result = await current.building;
|
|
445
|
+
if (current.rebuildQueued) {
|
|
446
|
+
current.rebuildQueued = false;
|
|
447
|
+
return this.rebuild(id);
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
async revert(id) {
|
|
452
|
+
const commit = await this.deps.repo.revertExtension(id);
|
|
453
|
+
await this.rebuild(id);
|
|
454
|
+
return commit;
|
|
455
|
+
}
|
|
456
|
+
/** Loads the host half of the current build (cached per build). */
|
|
457
|
+
loadHost(id) {
|
|
458
|
+
const entry = this.entries.get(id);
|
|
459
|
+
if (!entry?.buildId || !entry.hasHost) throw new Error(`"${id}" has no built host half`);
|
|
460
|
+
if (entry.host?.buildId === entry.buildId) return entry.host.extension;
|
|
461
|
+
const hostFile = join(entry.dir, "dist", "host.cjs");
|
|
462
|
+
delete nodeRequire.cache[nodeRequire.resolve(hostFile)];
|
|
463
|
+
const loaded = nodeRequire(hostFile);
|
|
464
|
+
const extension = loaded?.default ?? loaded;
|
|
465
|
+
if (typeof extension?.activate !== "function") throw new Error("host entry must `export default defineHostExtension({ activate })`");
|
|
466
|
+
entry.host = {
|
|
467
|
+
buildId: entry.buildId,
|
|
468
|
+
extension
|
|
469
|
+
};
|
|
470
|
+
return extension;
|
|
471
|
+
}
|
|
472
|
+
/** Resolves `<id>/<file>` from a `nubase-ext://<key>/<id>/<file>` URL to a file in dist/. */
|
|
473
|
+
resolveAsset(path) {
|
|
474
|
+
const [id, ...rest] = decodeURIComponent(path).replace(/^\//, "").split("/");
|
|
475
|
+
if (!id || !this.entries.has(id)) return void 0;
|
|
476
|
+
const dist = join(this.pathOf(id), "dist");
|
|
477
|
+
const file = join(dist, ...rest);
|
|
478
|
+
return file.startsWith(dist + sep) ? file : void 0;
|
|
479
|
+
}
|
|
480
|
+
dispose() {
|
|
481
|
+
this.watcher?.close();
|
|
482
|
+
for (const timer of this.pendingChanges.values()) clearTimeout(timer);
|
|
483
|
+
this.listeners.clear();
|
|
484
|
+
}
|
|
485
|
+
async discover(onlyId) {
|
|
486
|
+
if (!existsSync(this.deps.dir)) return;
|
|
487
|
+
const entries = await readdir(this.deps.dir, { withFileTypes: true });
|
|
488
|
+
for (const dirent of entries) {
|
|
489
|
+
if (!dirent.isDirectory() || dirent.name.startsWith(".")) continue;
|
|
490
|
+
if (onlyId && dirent.name !== onlyId) continue;
|
|
491
|
+
if (this.entries.has(dirent.name)) continue;
|
|
492
|
+
const dir = join(this.deps.dir, dirent.name);
|
|
493
|
+
if (!existsSync(join(dir, "package.json"))) continue;
|
|
494
|
+
this.entries.set(dirent.name, {
|
|
495
|
+
id: dirent.name,
|
|
496
|
+
dir,
|
|
497
|
+
displayName: dirent.name,
|
|
498
|
+
status: "building",
|
|
499
|
+
diagnostics: [],
|
|
500
|
+
hasHost: false
|
|
501
|
+
});
|
|
502
|
+
await this.rebuild(dirent.name);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async doBuild(entry) {
|
|
506
|
+
const { id, dir } = entry;
|
|
507
|
+
entry.status = "building";
|
|
508
|
+
await this.notify(id);
|
|
509
|
+
let manifest;
|
|
510
|
+
try {
|
|
511
|
+
manifest = await readManifest(dir);
|
|
512
|
+
if (manifest.id !== id) throw new ManifestError(`"nubase.id" (${manifest.id}) must match the folder name (${id})`);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return this.fail(entry, [{
|
|
515
|
+
source: "esbuild",
|
|
516
|
+
file: "package.json",
|
|
517
|
+
message: error.message
|
|
518
|
+
}]);
|
|
519
|
+
}
|
|
520
|
+
entry.displayName = manifest.displayName;
|
|
521
|
+
entry.description = manifest.description;
|
|
522
|
+
const result = await this.deps.builder.build(dir, manifest);
|
|
523
|
+
if (!result.ok) return this.fail(entry, result.diagnostics);
|
|
524
|
+
const url = (file) => `${EXTENSION_PROTOCOL}://${this.deps.key}/${id}/${file}?v=${result.buildId}`;
|
|
525
|
+
entry.status = "active";
|
|
526
|
+
entry.diagnostics = [];
|
|
527
|
+
entry.buildId = result.buildId;
|
|
528
|
+
entry.hasHost = result.hasHost;
|
|
529
|
+
entry.uiBundleUrl = result.hasUi ? url("ui.js") : void 0;
|
|
530
|
+
entry.uiStylesUrl = result.hasCss ? url("ui.css") : void 0;
|
|
531
|
+
await this.deps.repo.commitExtension(id, `nubase: ${id} built successfully`).catch((error) => console.warn(`Could not commit ${id}`, error));
|
|
532
|
+
await this.notify(id);
|
|
533
|
+
return entry;
|
|
534
|
+
}
|
|
535
|
+
async fail(entry, diagnostics) {
|
|
536
|
+
entry.status = "build-failed";
|
|
537
|
+
entry.diagnostics = diagnostics;
|
|
538
|
+
await this.notify(entry.id);
|
|
539
|
+
return entry;
|
|
540
|
+
}
|
|
541
|
+
async notify(id) {
|
|
542
|
+
await Promise.all([...this.listeners].map(async (listener) => {
|
|
543
|
+
try {
|
|
544
|
+
await listener(id);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error(`Extension listener failed for ${id}`, error);
|
|
547
|
+
}
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
watch() {
|
|
551
|
+
if (!existsSync(this.deps.dir)) return;
|
|
552
|
+
try {
|
|
553
|
+
this.watcher = watch(this.deps.dir, { recursive: true }, (_e, filename) => {
|
|
554
|
+
if (!filename) return;
|
|
555
|
+
const [id, ...rest] = relative(this.deps.dir, join(this.deps.dir, filename.toString())).split(sep);
|
|
556
|
+
if (!id || id.startsWith(".") || rest.length === 0) return;
|
|
557
|
+
if ([
|
|
558
|
+
"dist",
|
|
559
|
+
"node_modules",
|
|
560
|
+
".git"
|
|
561
|
+
].includes(rest[0] ?? "")) return;
|
|
562
|
+
clearTimeout(this.pendingChanges.get(id));
|
|
563
|
+
this.pendingChanges.set(id, setTimeout(() => {
|
|
564
|
+
this.pendingChanges.delete(id);
|
|
565
|
+
if (!existsSync(join(this.deps.dir, id, "package.json"))) return;
|
|
566
|
+
this.rebuild(id).catch((error) => console.error(`Rebuild of ${id} failed`, error));
|
|
567
|
+
}, 400));
|
|
568
|
+
});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.warn(`Could not watch ${this.deps.dir}`, error);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/main/services/extensions/git.ts
|
|
576
|
+
var exec = promisify(execFile);
|
|
577
|
+
/**
|
|
578
|
+
* The extensions folder is a git repository. Every successful build is
|
|
579
|
+
* committed, so a broken change can always be reverted to the last version
|
|
580
|
+
* that built.
|
|
581
|
+
*
|
|
582
|
+
* A folder's own extensions live inside the user's project, so their
|
|
583
|
+
* history goes to a separate `gitDir` under `~/.nubase` instead of a nested
|
|
584
|
+
* `.git` the project would see.
|
|
585
|
+
*/
|
|
586
|
+
var ExtensionsRepo = class {
|
|
587
|
+
available = true;
|
|
588
|
+
constructor(dir, gitDir) {
|
|
589
|
+
this.dir = dir;
|
|
590
|
+
this.gitDir = gitDir;
|
|
591
|
+
}
|
|
592
|
+
async ensure() {
|
|
593
|
+
try {
|
|
594
|
+
if (!existsSync(this.gitDir ?? join(this.dir, ".git"))) {
|
|
595
|
+
await this.git("init", "-q");
|
|
596
|
+
await this.commitAll("nubase: initialize extensions repository");
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
this.available = false;
|
|
600
|
+
console.warn(`git unavailable; extension history disabled: ${error.message}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Commits the extension folder if it changed; returns the short hash. */
|
|
604
|
+
async commitExtension(id, message) {
|
|
605
|
+
if (!this.available) return void 0;
|
|
606
|
+
await this.git("add", "-A", "--", id);
|
|
607
|
+
if (!await this.git("diff", "--cached", "--quiet", "--", id).then(() => false, () => true)) return void 0;
|
|
608
|
+
await this.git("commit", "-q", "-m", message, "--", id);
|
|
609
|
+
return (await this.git("rev-parse", "--short", "HEAD")).trim();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Undoes the latest change to an extension. Uncommitted edits are discarded
|
|
613
|
+
* first (back to the last successful build); with none, the extension steps
|
|
614
|
+
* back one build — a version can build and still misbehave at runtime.
|
|
615
|
+
* Repeated calls walk further back through history.
|
|
616
|
+
*/
|
|
617
|
+
async revertExtension(id) {
|
|
618
|
+
if (!this.available) throw new Error("git is not available");
|
|
619
|
+
if (!(await this.git("ls-files", "--", id)).trim()) throw new Error(`"${id}" has never built successfully, so there is nothing to revert to`);
|
|
620
|
+
if ((await this.git("status", "--porcelain", "--", id)).trim()) {
|
|
621
|
+
await this.git("checkout", "HEAD", "--", id);
|
|
622
|
+
await this.git("clean", "-fdq", "--", id);
|
|
623
|
+
return `discarded unsaved changes; back to build ${(await this.git("log", "-1", "--format=%h", "--", id)).trim()}`;
|
|
624
|
+
}
|
|
625
|
+
const [, previous] = (await this.git("log", "-2", "--format=%h", "--", id)).trim().split("\n");
|
|
626
|
+
if (!previous) throw new Error(`"${id}" has no earlier version to revert to`);
|
|
627
|
+
await this.git("rm", "-rq", "--cached", "--ignore-unmatch", "--", id);
|
|
628
|
+
await this.git("clean", "-fdq", "--", id);
|
|
629
|
+
await this.git("checkout", previous, "--", id);
|
|
630
|
+
await this.git("add", "-A", "--", id);
|
|
631
|
+
await this.git("commit", "-q", "-m", `nubase: revert ${id} to ${previous}`, "--", id);
|
|
632
|
+
return `reverted to build ${previous}`;
|
|
633
|
+
}
|
|
634
|
+
async log(id, limit = 10) {
|
|
635
|
+
if (!this.available) return "";
|
|
636
|
+
return this.git("log", `-${limit}`, "--format=%h %ad %s", "--date=relative", "--", id);
|
|
637
|
+
}
|
|
638
|
+
async commitAll(message) {
|
|
639
|
+
await this.git("add", "-A");
|
|
640
|
+
await this.git("commit", "-q", "--allow-empty", "-m", message);
|
|
641
|
+
}
|
|
642
|
+
async git(...args) {
|
|
643
|
+
const identity = args[0] === "commit" ? [
|
|
644
|
+
"-c",
|
|
645
|
+
"user.name=Nubase",
|
|
646
|
+
"-c",
|
|
647
|
+
"user.email=nubase@localhost"
|
|
648
|
+
] : [];
|
|
649
|
+
const { stdout } = await exec("git", [
|
|
650
|
+
...this.gitDir ? [`--git-dir=${this.gitDir}`, `--work-tree=${this.dir}`] : [],
|
|
651
|
+
...identity,
|
|
652
|
+
...args
|
|
653
|
+
], {
|
|
654
|
+
cwd: this.dir,
|
|
655
|
+
maxBuffer: 10 * 1024 * 1024
|
|
656
|
+
});
|
|
657
|
+
return stdout;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/main/services/extensions/workspace.ts
|
|
662
|
+
/**
|
|
663
|
+
* Prepares the extensions folder: shared tsconfig (pointing at the types
|
|
664
|
+
* this Nubase version ships), agent guidance and git ignores. Rewritten on
|
|
665
|
+
* every start so it always matches the running app.
|
|
666
|
+
*/
|
|
667
|
+
var prepareExtensionsWorkspace = async (dir, resourcesDir, options = {}) => {
|
|
668
|
+
await mkdir(dir, { recursive: true });
|
|
669
|
+
const tsconfig = { compilerOptions: {
|
|
670
|
+
target: "ES2022",
|
|
671
|
+
module: "ESNext",
|
|
672
|
+
moduleResolution: "bundler",
|
|
673
|
+
jsx: "react-jsx",
|
|
674
|
+
lib: [
|
|
675
|
+
"ES2023",
|
|
676
|
+
"DOM",
|
|
677
|
+
"DOM.Iterable"
|
|
678
|
+
],
|
|
679
|
+
strict: true,
|
|
680
|
+
skipLibCheck: true,
|
|
681
|
+
noEmit: true,
|
|
682
|
+
isolatedModules: true,
|
|
683
|
+
esModuleInterop: true,
|
|
684
|
+
types: ["node"],
|
|
685
|
+
typeRoots: resolveTypeRoots(),
|
|
686
|
+
paths: resolveTypePaths()
|
|
687
|
+
} };
|
|
688
|
+
await writeFile(join(dir, "tsconfig.base.json"), `// Generated by Nubase on startup. Do not edit.\n${JSON.stringify(tsconfig, null, 2)}\n`);
|
|
689
|
+
await writeFile(join(dir, ".gitignore"), options.inProject ? "node_modules/\ndist/\ntsconfig.base.json\nAGENTS.md\n" : "node_modules/\ndist/\n");
|
|
690
|
+
await cp(join(resourcesDir, "skills", "nubase-extension-authoring", "SKILL.md"), join(dir, "AGENTS.md"));
|
|
691
|
+
};
|
|
692
|
+
/** Creates a new extension from the bundled template. */
|
|
693
|
+
var scaffoldExtension = async (extensionsDir, templateDir, options) => {
|
|
694
|
+
if (!isValidExtensionId(options.id)) throw new Error(`Invalid id "${options.id}": use lowercase kebab-case, e.g. "jira-tickets"`);
|
|
695
|
+
const target = join(extensionsDir, options.id);
|
|
696
|
+
if (existsSync(target)) throw new Error(`An extension already exists at ${target}`);
|
|
697
|
+
const replacements = {
|
|
698
|
+
__ID__: options.id,
|
|
699
|
+
__DISPLAY_NAME__: options.displayName,
|
|
700
|
+
__DESCRIPTION__: options.description ?? "",
|
|
701
|
+
__WIDGET_ID__: `${options.id}.main`
|
|
702
|
+
};
|
|
703
|
+
const copy = async (from, to) => {
|
|
704
|
+
await mkdir(to, { recursive: true });
|
|
705
|
+
for (const entry of await readdir(from, { withFileTypes: true })) {
|
|
706
|
+
const name = entry.name.replace(/\.template$/, "");
|
|
707
|
+
if (!options.withHost && name === "host.ts") continue;
|
|
708
|
+
if (entry.isDirectory()) {
|
|
709
|
+
await copy(join(from, entry.name), join(to, name));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
let content = await readFile(join(from, entry.name), "utf8");
|
|
713
|
+
for (const [token, value] of Object.entries(replacements)) content = content.replaceAll(token, value);
|
|
714
|
+
if (name === "package.json" && !options.withHost) {
|
|
715
|
+
const pkg = JSON.parse(content);
|
|
716
|
+
delete pkg.nubase.host;
|
|
717
|
+
content = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
718
|
+
}
|
|
719
|
+
await writeFile(join(to, name), content);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
await copy(templateDir, target);
|
|
723
|
+
return target;
|
|
724
|
+
};
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/main/services/fs/fileProtocol.ts
|
|
727
|
+
var MIME_TYPES$1 = {
|
|
728
|
+
apng: "image/apng",
|
|
729
|
+
avif: "image/avif",
|
|
730
|
+
bmp: "image/bmp",
|
|
731
|
+
gif: "image/gif",
|
|
732
|
+
ico: "image/x-icon",
|
|
733
|
+
jpeg: "image/jpeg",
|
|
734
|
+
jpg: "image/jpeg",
|
|
735
|
+
png: "image/png",
|
|
736
|
+
svg: "image/svg+xml",
|
|
737
|
+
webp: "image/webp",
|
|
738
|
+
mp4: "video/mp4",
|
|
739
|
+
m4v: "video/mp4",
|
|
740
|
+
mov: "video/quicktime",
|
|
741
|
+
webm: "video/webm",
|
|
742
|
+
ogv: "video/ogg",
|
|
743
|
+
mkv: "video/x-matroska",
|
|
744
|
+
mp3: "audio/mpeg",
|
|
745
|
+
wav: "audio/wav",
|
|
746
|
+
ogg: "audio/ogg",
|
|
747
|
+
oga: "audio/ogg",
|
|
748
|
+
opus: "audio/opus",
|
|
749
|
+
m4a: "audio/mp4",
|
|
750
|
+
aac: "audio/aac",
|
|
751
|
+
flac: "audio/flac",
|
|
752
|
+
weba: "audio/webm",
|
|
753
|
+
pdf: "application/pdf",
|
|
754
|
+
html: "text/html; charset=utf-8",
|
|
755
|
+
htm: "text/html; charset=utf-8",
|
|
756
|
+
css: "text/css; charset=utf-8",
|
|
757
|
+
js: "text/javascript; charset=utf-8",
|
|
758
|
+
mjs: "text/javascript; charset=utf-8",
|
|
759
|
+
json: "application/json; charset=utf-8",
|
|
760
|
+
txt: "text/plain; charset=utf-8",
|
|
761
|
+
md: "text/markdown; charset=utf-8",
|
|
762
|
+
csv: "text/csv; charset=utf-8",
|
|
763
|
+
xml: "application/xml",
|
|
764
|
+
ttf: "font/ttf",
|
|
765
|
+
otf: "font/otf",
|
|
766
|
+
woff: "font/woff",
|
|
767
|
+
woff2: "font/woff2"
|
|
768
|
+
};
|
|
769
|
+
var mimeTypeOf = (path) => MIME_TYPES$1[extname(path).slice(1).toLowerCase()] ?? "application/octet-stream";
|
|
770
|
+
var parseRange = (header, size) => {
|
|
771
|
+
const match = header?.match(/^bytes=(\d*)-(\d*)$/);
|
|
772
|
+
if (!match) return void 0;
|
|
773
|
+
const [, from = "", to = ""] = match;
|
|
774
|
+
if (!from && !to) return "invalid";
|
|
775
|
+
const start = from ? Number(from) : Math.max(0, size - Number(to));
|
|
776
|
+
const end = from && to ? Math.min(Number(to), size - 1) : size - 1;
|
|
777
|
+
if (start > end || start >= size) return "invalid";
|
|
778
|
+
return {
|
|
779
|
+
start,
|
|
780
|
+
end
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
/** Serves `nubase-file://local/<path>` with Range support, so video can seek and large files stream. */
|
|
784
|
+
var handleFileRequest = async (request) => {
|
|
785
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method not allowed", { status: 405 });
|
|
786
|
+
const path = fromFileUrl(request.url);
|
|
787
|
+
if (!path) return new Response("Not found", { status: 404 });
|
|
788
|
+
let size;
|
|
789
|
+
try {
|
|
790
|
+
const info = await stat(path);
|
|
791
|
+
if (!info.isFile()) return new Response("Not a file", { status: 404 });
|
|
792
|
+
size = info.size;
|
|
793
|
+
} catch {
|
|
794
|
+
return new Response("Not found", { status: 404 });
|
|
795
|
+
}
|
|
796
|
+
const headers = {
|
|
797
|
+
"Content-Type": mimeTypeOf(path),
|
|
798
|
+
"Accept-Ranges": "bytes",
|
|
799
|
+
"Access-Control-Allow-Origin": "*",
|
|
800
|
+
"Cache-Control": "no-store",
|
|
801
|
+
"X-Content-Type-Options": "nosniff"
|
|
802
|
+
};
|
|
803
|
+
const range = parseRange(request.headers.get("Range"), size);
|
|
804
|
+
if (range === "invalid") return new Response(null, {
|
|
805
|
+
status: 416,
|
|
806
|
+
headers: {
|
|
807
|
+
...headers,
|
|
808
|
+
"Content-Range": `bytes */${size}`
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
const { start, end } = range ?? {
|
|
812
|
+
start: 0,
|
|
813
|
+
end: size - 1
|
|
814
|
+
};
|
|
815
|
+
const length = size === 0 ? 0 : end - start + 1;
|
|
816
|
+
headers["Content-Length"] = String(length);
|
|
817
|
+
if (range) headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
|
|
818
|
+
const body = request.method === "HEAD" || length === 0 ? null : Readable.toWeb(createReadStream(path, {
|
|
819
|
+
start,
|
|
820
|
+
end
|
|
821
|
+
}));
|
|
822
|
+
return new Response(body, {
|
|
823
|
+
status: range ? 206 : 200,
|
|
824
|
+
headers
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/main/services/fs/LocalFileSystem.ts
|
|
829
|
+
var fileType = (entry) => {
|
|
830
|
+
if (entry.isFile()) return "file";
|
|
831
|
+
if (entry.isDirectory()) return "directory";
|
|
832
|
+
if (entry.isSymbolicLink()) return "symlink";
|
|
833
|
+
return "other";
|
|
834
|
+
};
|
|
835
|
+
/** FileSystem backed by the machine's disk. */
|
|
836
|
+
var LocalFileSystem = class {
|
|
837
|
+
homeDir = homedir();
|
|
838
|
+
async readText(path) {
|
|
839
|
+
return readFile(resolve(path), "utf8");
|
|
840
|
+
}
|
|
841
|
+
async writeText(path, content) {
|
|
842
|
+
const target = resolve(path);
|
|
843
|
+
await mkdir(dirname(target), { recursive: true });
|
|
844
|
+
await writeFile(target, content, "utf8");
|
|
845
|
+
}
|
|
846
|
+
async stat(path) {
|
|
847
|
+
try {
|
|
848
|
+
const s = await stat(resolve(path));
|
|
849
|
+
return {
|
|
850
|
+
type: fileType(s),
|
|
851
|
+
size: s.size,
|
|
852
|
+
mtimeMs: s.mtimeMs
|
|
853
|
+
};
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (error.code === "ENOENT") return void 0;
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async readDirectory(path) {
|
|
860
|
+
const dir = resolve(path);
|
|
861
|
+
return (await readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
862
|
+
name: entry.name,
|
|
863
|
+
path: join(dir, entry.name),
|
|
864
|
+
type: fileType(entry)
|
|
865
|
+
})).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
866
|
+
}
|
|
867
|
+
async mkdir(path) {
|
|
868
|
+
await mkdir(resolve(path), { recursive: true });
|
|
869
|
+
}
|
|
870
|
+
async delete(path, options) {
|
|
871
|
+
await rm(resolve(path), { recursive: options?.recursive ?? false });
|
|
872
|
+
}
|
|
873
|
+
async rename(from, to) {
|
|
874
|
+
await rename(resolve(from), resolve(to));
|
|
875
|
+
}
|
|
876
|
+
watch(path, listener, options) {
|
|
877
|
+
const root = resolve(path);
|
|
878
|
+
const watcher = watch(root, { recursive: options?.recursive ?? false }, (eventType, filename) => {
|
|
879
|
+
const changed = filename ? join(root, filename.toString()) : root;
|
|
880
|
+
if (eventType === "change") {
|
|
881
|
+
listener({
|
|
882
|
+
type: "changed",
|
|
883
|
+
path: changed
|
|
884
|
+
});
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
stat(changed).then(() => listener({
|
|
888
|
+
type: "created",
|
|
889
|
+
path: changed
|
|
890
|
+
}), () => listener({
|
|
891
|
+
type: "deleted",
|
|
892
|
+
path: changed
|
|
893
|
+
}));
|
|
894
|
+
});
|
|
895
|
+
watcher.on("error", () => watcher.close());
|
|
896
|
+
return { dispose: () => watcher.close() };
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/main/services/store/JsonFile.ts
|
|
901
|
+
/**
|
|
902
|
+
* A JSON document on disk. Writes are atomic, and external edits (by the
|
|
903
|
+
* user or the agent) are reported through `onExternalChange`.
|
|
904
|
+
*/
|
|
905
|
+
var JsonFile = class {
|
|
906
|
+
lastWritten;
|
|
907
|
+
watcher;
|
|
908
|
+
constructor(path) {
|
|
909
|
+
this.path = path;
|
|
910
|
+
}
|
|
911
|
+
async read() {
|
|
912
|
+
try {
|
|
913
|
+
return JSON.parse(await readFile(this.path, "utf8"));
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (error.code === "ENOENT") return void 0;
|
|
916
|
+
if (error instanceof SyntaxError) {
|
|
917
|
+
console.error(`Ignoring invalid JSON in ${this.path}: ${error.message}`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async write(value) {
|
|
924
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
925
|
+
this.lastWritten = content;
|
|
926
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
927
|
+
const temp = `${this.path}.${process.pid}.tmp`;
|
|
928
|
+
await writeFile(temp, content, "utf8");
|
|
929
|
+
await rename(temp, this.path);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* With `createDir: false` a missing parent folder is not created; the
|
|
933
|
+
* file is picked up once someone creates the folder.
|
|
934
|
+
*/
|
|
935
|
+
onExternalChange(listener, options = {}) {
|
|
936
|
+
const dir = dirname(this.path);
|
|
937
|
+
if (options.createDir === false && !existsSync(dir)) {
|
|
938
|
+
const parent = dirname(dir);
|
|
939
|
+
if (!existsSync(parent)) return () => {};
|
|
940
|
+
this.watcher = watch(parent, (_event, filename) => {
|
|
941
|
+
if (filename?.toString() !== basename(dir) || !existsSync(dir)) return;
|
|
942
|
+
this.watcher?.close();
|
|
943
|
+
this.watchFile(listener);
|
|
944
|
+
this.read().then((value) => {
|
|
945
|
+
if (value !== void 0) listener(value);
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
return () => this.watcher?.close();
|
|
949
|
+
}
|
|
950
|
+
mkdirSync(dir, { recursive: true });
|
|
951
|
+
this.watchFile(listener);
|
|
952
|
+
return () => this.watcher?.close();
|
|
953
|
+
}
|
|
954
|
+
watchFile(listener) {
|
|
955
|
+
let timer;
|
|
956
|
+
const name = basename(this.path);
|
|
957
|
+
this.watcher = watch(dirname(this.path), (_event, filename) => {
|
|
958
|
+
if (filename?.toString() !== name) return;
|
|
959
|
+
clearTimeout(timer);
|
|
960
|
+
timer = setTimeout(async () => {
|
|
961
|
+
try {
|
|
962
|
+
const content = await readFile(join(dirname(this.path), name), "utf8");
|
|
963
|
+
if (content === this.lastWritten) return;
|
|
964
|
+
this.lastWritten = content;
|
|
965
|
+
listener(JSON.parse(content));
|
|
966
|
+
} catch {}
|
|
967
|
+
}, 100);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/main/services/store/SecretStore.ts
|
|
973
|
+
/**
|
|
974
|
+
* Secrets encrypted with the OS keychain (Electron safeStorage) and kept in
|
|
975
|
+
* a JSON file. Values never travel to the renderer or the model.
|
|
976
|
+
*/
|
|
977
|
+
var SecretStore = class {
|
|
978
|
+
file;
|
|
979
|
+
constructor(path) {
|
|
980
|
+
this.file = new JsonFile(path);
|
|
981
|
+
}
|
|
982
|
+
async get(key) {
|
|
983
|
+
const encoded = (await this.file.read())?.[key];
|
|
984
|
+
if (!encoded) return void 0;
|
|
985
|
+
const buffer = Buffer.from(encoded, "base64");
|
|
986
|
+
return safeStorage.isEncryptionAvailable() ? safeStorage.decryptString(buffer) : buffer.toString("utf8");
|
|
987
|
+
}
|
|
988
|
+
async set(key, value) {
|
|
989
|
+
const data = await this.file.read() ?? {};
|
|
990
|
+
data[key] = (safeStorage.isEncryptionAvailable() ? safeStorage.encryptString(value) : Buffer.from(value, "utf8")).toString("base64");
|
|
991
|
+
await this.file.write(data);
|
|
992
|
+
}
|
|
993
|
+
async delete(key) {
|
|
994
|
+
const data = await this.file.read() ?? {};
|
|
995
|
+
delete data[key];
|
|
996
|
+
await this.file.write(data);
|
|
997
|
+
}
|
|
998
|
+
async keys(prefix) {
|
|
999
|
+
return Object.keys(await this.file.read() ?? {}).filter((key) => key.startsWith(prefix));
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/main/services/store/SettingsService.ts
|
|
1004
|
+
var defaultSettings = {
|
|
1005
|
+
theme: "dark",
|
|
1006
|
+
keybindings: [],
|
|
1007
|
+
disabledExtensions: [],
|
|
1008
|
+
agent: {}
|
|
1009
|
+
};
|
|
1010
|
+
var SettingsService = class {
|
|
1011
|
+
file;
|
|
1012
|
+
current = defaultSettings;
|
|
1013
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1014
|
+
constructor(path) {
|
|
1015
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1016
|
+
this.file = new JsonFile(path);
|
|
1017
|
+
}
|
|
1018
|
+
async load() {
|
|
1019
|
+
this.current = {
|
|
1020
|
+
...defaultSettings,
|
|
1021
|
+
...await this.file.read()
|
|
1022
|
+
};
|
|
1023
|
+
this.file.onExternalChange((value) => {
|
|
1024
|
+
this.current = {
|
|
1025
|
+
...defaultSettings,
|
|
1026
|
+
...value
|
|
1027
|
+
};
|
|
1028
|
+
this.notify();
|
|
1029
|
+
});
|
|
1030
|
+
return this.current;
|
|
1031
|
+
}
|
|
1032
|
+
get() {
|
|
1033
|
+
return this.current;
|
|
1034
|
+
}
|
|
1035
|
+
async update(patch) {
|
|
1036
|
+
this.current = {
|
|
1037
|
+
...this.current,
|
|
1038
|
+
...patch
|
|
1039
|
+
};
|
|
1040
|
+
await this.file.write(this.current);
|
|
1041
|
+
this.notify();
|
|
1042
|
+
return this.current;
|
|
1043
|
+
}
|
|
1044
|
+
onChange(listener) {
|
|
1045
|
+
this.listeners.add(listener);
|
|
1046
|
+
return () => this.listeners.delete(listener);
|
|
1047
|
+
}
|
|
1048
|
+
notify() {
|
|
1049
|
+
for (const listener of this.listeners) listener(this.current);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/main/services/store/StorageService.ts
|
|
1054
|
+
/** Per-scope (usually per-extension) key/value storage, one JSON file each. */
|
|
1055
|
+
var StorageService = class {
|
|
1056
|
+
files = /* @__PURE__ */ new Map();
|
|
1057
|
+
constructor(dir) {
|
|
1058
|
+
this.dir = dir;
|
|
1059
|
+
}
|
|
1060
|
+
async get(scope, key) {
|
|
1061
|
+
return (await this.file(scope).read())?.[key];
|
|
1062
|
+
}
|
|
1063
|
+
async set(scope, key, value) {
|
|
1064
|
+
const file = this.file(scope);
|
|
1065
|
+
const data = await file.read() ?? {};
|
|
1066
|
+
if (value === void 0) delete data[key];
|
|
1067
|
+
else data[key] = value;
|
|
1068
|
+
await file.write(data);
|
|
1069
|
+
}
|
|
1070
|
+
file(scope) {
|
|
1071
|
+
const safe = scope.replace(/[^\w.-]/g, "_");
|
|
1072
|
+
let file = this.files.get(safe);
|
|
1073
|
+
if (!file) {
|
|
1074
|
+
file = new JsonFile(join(this.dir, `${safe}.json`));
|
|
1075
|
+
this.files.set(safe, file);
|
|
1076
|
+
}
|
|
1077
|
+
return file;
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/builtin-extensions/files/host.ts
|
|
1082
|
+
var MAX_ENTRIES = 1e4;
|
|
1083
|
+
/** Reads only the zip's central directory, so listing a huge archive stays cheap. */
|
|
1084
|
+
var listZip = (path) => new Promise((resolve, reject) => {
|
|
1085
|
+
yauzl.open(path, {
|
|
1086
|
+
lazyEntries: true,
|
|
1087
|
+
autoClose: true
|
|
1088
|
+
}, (error, zip) => {
|
|
1089
|
+
if (error || !zip) {
|
|
1090
|
+
reject(error ?? /* @__PURE__ */ new Error("Could not open archive"));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const entries = [];
|
|
1094
|
+
zip.on("entry", (entry) => {
|
|
1095
|
+
entries.push({
|
|
1096
|
+
name: entry.fileName,
|
|
1097
|
+
size: entry.uncompressedSize,
|
|
1098
|
+
compressedSize: entry.compressedSize,
|
|
1099
|
+
isDirectory: entry.fileName.endsWith("/"),
|
|
1100
|
+
modified: entry.getLastModDate().getTime()
|
|
1101
|
+
});
|
|
1102
|
+
if (entries.length >= MAX_ENTRIES) {
|
|
1103
|
+
zip.close();
|
|
1104
|
+
resolve({
|
|
1105
|
+
entries,
|
|
1106
|
+
truncated: true
|
|
1107
|
+
});
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
zip.readEntry();
|
|
1111
|
+
});
|
|
1112
|
+
zip.on("end", () => resolve({
|
|
1113
|
+
entries,
|
|
1114
|
+
truncated: false
|
|
1115
|
+
}));
|
|
1116
|
+
zip.on("error", reject);
|
|
1117
|
+
zip.readEntry();
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
var createFilesHost = () => defineHostExtension({ activate(ctx) {
|
|
1121
|
+
return [ctx.rpc.handle("listZip", (path) => listZip(path))];
|
|
1122
|
+
} });
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/builtin-extensions/manifest.ts
|
|
1125
|
+
/** Built-in extensions. Their ui halves load in the renderer, host halves in main. */
|
|
1126
|
+
var BUILTIN_EXTENSIONS = [
|
|
1127
|
+
{
|
|
1128
|
+
id: "workbench",
|
|
1129
|
+
displayName: "Workbench",
|
|
1130
|
+
description: "Dock, palette and core commands"
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
id: "chat",
|
|
1134
|
+
displayName: "Agent Chat",
|
|
1135
|
+
description: "Talk to the pi agent"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
id: "files",
|
|
1139
|
+
displayName: "Files",
|
|
1140
|
+
description: "Browse and preview files"
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
id: "terminal",
|
|
1144
|
+
displayName: "Terminal",
|
|
1145
|
+
description: "Shell sessions"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
id: "extensions",
|
|
1149
|
+
displayName: "Extensions",
|
|
1150
|
+
description: "Manage extensions"
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
id: "settings",
|
|
1154
|
+
displayName: "Settings",
|
|
1155
|
+
description: "Theme, keybindings and agent setup"
|
|
1156
|
+
}
|
|
1157
|
+
];
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/main/services/pty/PtyHost.ts
|
|
1160
|
+
var PtyHost = class {
|
|
1161
|
+
ptys = /* @__PURE__ */ new Map();
|
|
1162
|
+
constructor(options) {
|
|
1163
|
+
this.options = options;
|
|
1164
|
+
}
|
|
1165
|
+
async spawn(options) {
|
|
1166
|
+
const pty = await import("node-pty");
|
|
1167
|
+
const shell = process.env.SHELL ?? (process.platform === "win32" ? "powershell.exe" : "/bin/zsh");
|
|
1168
|
+
const id = randomUUID();
|
|
1169
|
+
const instance = pty.spawn(shell, ["-l"], {
|
|
1170
|
+
name: "xterm-256color",
|
|
1171
|
+
cols: options.cols,
|
|
1172
|
+
rows: options.rows,
|
|
1173
|
+
cwd: options.cwd ?? homedir(),
|
|
1174
|
+
env: {
|
|
1175
|
+
...process.env,
|
|
1176
|
+
...this.options.env,
|
|
1177
|
+
TERM_PROGRAM: "nubase",
|
|
1178
|
+
COLORTERM: "truecolor"
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
instance.onData((data) => this.options.onData(id, data));
|
|
1182
|
+
instance.onExit(({ exitCode }) => {
|
|
1183
|
+
this.ptys.delete(id);
|
|
1184
|
+
this.options.onExit(id, exitCode);
|
|
1185
|
+
});
|
|
1186
|
+
this.ptys.set(id, instance);
|
|
1187
|
+
return id;
|
|
1188
|
+
}
|
|
1189
|
+
write(id, data) {
|
|
1190
|
+
this.ptys.get(id)?.write(data);
|
|
1191
|
+
}
|
|
1192
|
+
resize(id, cols, rows) {
|
|
1193
|
+
if (cols > 0 && rows > 0) this.ptys.get(id)?.resize(cols, rows);
|
|
1194
|
+
}
|
|
1195
|
+
kill(id) {
|
|
1196
|
+
this.ptys.get(id)?.kill();
|
|
1197
|
+
this.ptys.delete(id);
|
|
1198
|
+
}
|
|
1199
|
+
dispose() {
|
|
1200
|
+
for (const id of [...this.ptys.keys()]) this.kill(id);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/builtin-extensions/terminal/host.ts
|
|
1205
|
+
/** Shells for the terminal widget. Uses only the public host API plus node-pty. */
|
|
1206
|
+
var createTerminalHost = (env) => defineHostExtension({ activate(ctx) {
|
|
1207
|
+
const ptys = new PtyHost({
|
|
1208
|
+
env,
|
|
1209
|
+
onData: (id, data) => ctx.rpc.emit("data", {
|
|
1210
|
+
id,
|
|
1211
|
+
data
|
|
1212
|
+
}),
|
|
1213
|
+
onExit: (id, exitCode) => ctx.rpc.emit("exit", {
|
|
1214
|
+
id,
|
|
1215
|
+
exitCode
|
|
1216
|
+
})
|
|
1217
|
+
});
|
|
1218
|
+
return [
|
|
1219
|
+
ctx.rpc.handle("spawn", (options) => ptys.spawn(options)),
|
|
1220
|
+
ctx.rpc.handle("write", (id, data) => ptys.write(id, data)),
|
|
1221
|
+
ctx.rpc.handle("resize", (id, cols, rows) => ptys.resize(id, cols, rows)),
|
|
1222
|
+
ctx.rpc.handle("kill", (id) => ptys.kill(id)),
|
|
1223
|
+
{ dispose: () => ptys.dispose() }
|
|
1224
|
+
];
|
|
1225
|
+
} });
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region src/main/rpc/electronTransport.ts
|
|
1228
|
+
var handlers = /* @__PURE__ */ new Map();
|
|
1229
|
+
var installed = false;
|
|
1230
|
+
/** One IPC handler for every view; each message goes to its sender's server. */
|
|
1231
|
+
var installRouter = () => {
|
|
1232
|
+
if (installed) return;
|
|
1233
|
+
installed = true;
|
|
1234
|
+
ipcMain.handle(INVOKE_CHANNEL, (event, method, args) => {
|
|
1235
|
+
const handler = handlers.get(event.sender);
|
|
1236
|
+
if (!handler) throw new Error("Rejected IPC from an unknown sender");
|
|
1237
|
+
return handler(method, args);
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1240
|
+
var createElectronTransport = (contents) => {
|
|
1241
|
+
installRouter();
|
|
1242
|
+
contents.once("destroyed", () => handlers.delete(contents));
|
|
1243
|
+
return {
|
|
1244
|
+
onInvoke(handler) {
|
|
1245
|
+
handlers.set(contents, handler);
|
|
1246
|
+
},
|
|
1247
|
+
send(event, payload) {
|
|
1248
|
+
if (!contents.isDestroyed()) contents.send(EVENT_CHANNEL, event, payload);
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
};
|
|
1252
|
+
//#endregion
|
|
1253
|
+
//#region src/main/rpc/HostServer.ts
|
|
1254
|
+
var HostServer = class {
|
|
1255
|
+
handlers = {};
|
|
1256
|
+
pending = /* @__PURE__ */ new Map();
|
|
1257
|
+
transport;
|
|
1258
|
+
constructor() {
|
|
1259
|
+
this.handlers["renderer.respond"] = (requestId, result) => {
|
|
1260
|
+
const request = this.pending.get(requestId);
|
|
1261
|
+
if (!request) return;
|
|
1262
|
+
this.pending.delete(requestId);
|
|
1263
|
+
clearTimeout(request.timer);
|
|
1264
|
+
if (result.ok) request.resolve(result.value);
|
|
1265
|
+
else request.reject(new Error(result.error));
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
connect(transport) {
|
|
1269
|
+
this.transport = transport;
|
|
1270
|
+
transport.onInvoke(async (method, args) => {
|
|
1271
|
+
const handler = this.handlers[method];
|
|
1272
|
+
if (!handler) throw new Error(`Unknown host method: ${method}`);
|
|
1273
|
+
return handler(...args);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
handle(handlers) {
|
|
1277
|
+
Object.assign(this.handlers, handlers);
|
|
1278
|
+
}
|
|
1279
|
+
emit(event, payload) {
|
|
1280
|
+
this.transport?.send(event, payload);
|
|
1281
|
+
}
|
|
1282
|
+
/** Asks the UI to do something only it can do (layout, dialogs, commands). */
|
|
1283
|
+
requestRenderer(method, ...args) {
|
|
1284
|
+
if (!this.transport) return Promise.reject(/* @__PURE__ */ new Error("No UI is connected"));
|
|
1285
|
+
const requestId = randomUUID();
|
|
1286
|
+
const interactive = method.startsWith("dialogs.");
|
|
1287
|
+
return new Promise((resolve, reject) => {
|
|
1288
|
+
const timer = setTimeout(() => {
|
|
1289
|
+
this.pending.delete(requestId);
|
|
1290
|
+
reject(/* @__PURE__ */ new Error(`UI did not answer ${method} in time`));
|
|
1291
|
+
}, interactive ? 10 * 6e4 : 15e3);
|
|
1292
|
+
this.pending.set(requestId, {
|
|
1293
|
+
resolve,
|
|
1294
|
+
reject,
|
|
1295
|
+
timer
|
|
1296
|
+
});
|
|
1297
|
+
this.emit("renderer.request", {
|
|
1298
|
+
requestId,
|
|
1299
|
+
method,
|
|
1300
|
+
args
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region src/main/services/agent/harness/AgentHarness.ts
|
|
1307
|
+
/** Thrown when a call needs a runtime or session that is not up yet. */
|
|
1308
|
+
var HarnessNotReadyError = class extends Error {
|
|
1309
|
+
constructor() {
|
|
1310
|
+
super("The agent is still starting");
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
//#endregion
|
|
1314
|
+
//#region src/main/services/agent/Transcript.ts
|
|
1315
|
+
/**
|
|
1316
|
+
* Projects a harness's event stream into a serializable list of ChatMessages.
|
|
1317
|
+
* The host owns this so every UI (and a future remote UI) renders the same
|
|
1318
|
+
* thing, whichever harness runs the agent.
|
|
1319
|
+
*/
|
|
1320
|
+
var Transcript = class {
|
|
1321
|
+
messages = [];
|
|
1322
|
+
toolParts = /* @__PURE__ */ new Map();
|
|
1323
|
+
counter = 0;
|
|
1324
|
+
constructor(onMessageChanged) {
|
|
1325
|
+
this.onMessageChanged = onMessageChanged;
|
|
1326
|
+
}
|
|
1327
|
+
get all() {
|
|
1328
|
+
return this.messages;
|
|
1329
|
+
}
|
|
1330
|
+
reset(history) {
|
|
1331
|
+
this.messages = [];
|
|
1332
|
+
this.toolParts.clear();
|
|
1333
|
+
for (const message of history) if (message.role === "toolResult") this.applyToolResult(message.toolCallId, message.output, message.isError, void 0, false);
|
|
1334
|
+
else this.startMessage(message, false);
|
|
1335
|
+
}
|
|
1336
|
+
apply(event) {
|
|
1337
|
+
switch (event.type) {
|
|
1338
|
+
case "message_start":
|
|
1339
|
+
if (event.message.role !== "toolResult") this.startMessage(event.message, true);
|
|
1340
|
+
break;
|
|
1341
|
+
case "message_update": {
|
|
1342
|
+
const { message } = event;
|
|
1343
|
+
if (message.role === "toolResult") break;
|
|
1344
|
+
const current = this.messages.at(-1);
|
|
1345
|
+
if (current && current.role === message.role) {
|
|
1346
|
+
this.fillParts(current, message);
|
|
1347
|
+
this.onMessageChanged(current);
|
|
1348
|
+
}
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
case "tool_start":
|
|
1352
|
+
this.updateTool(event.toolCallId, (part) => {
|
|
1353
|
+
part.status = "running";
|
|
1354
|
+
part.args = event.args ?? part.args;
|
|
1355
|
+
});
|
|
1356
|
+
break;
|
|
1357
|
+
case "tool_update":
|
|
1358
|
+
this.updateTool(event.toolCallId, (part) => {
|
|
1359
|
+
part.output = event.output ?? part.output;
|
|
1360
|
+
});
|
|
1361
|
+
break;
|
|
1362
|
+
case "tool_end":
|
|
1363
|
+
this.applyToolResult(event.toolCallId, event.output, event.isError, event.durationMs, true);
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
startMessage(message, notify) {
|
|
1368
|
+
const chat = {
|
|
1369
|
+
id: `m${this.counter++}`,
|
|
1370
|
+
role: message.role,
|
|
1371
|
+
parts: [],
|
|
1372
|
+
timestamp: message.timestamp ?? Date.now()
|
|
1373
|
+
};
|
|
1374
|
+
this.fillParts(chat, message);
|
|
1375
|
+
this.messages.push(chat);
|
|
1376
|
+
if (notify) this.onMessageChanged(chat);
|
|
1377
|
+
}
|
|
1378
|
+
fillParts(chat, message) {
|
|
1379
|
+
chat.parts = message.content.map((item) => {
|
|
1380
|
+
if (item.type !== "toolCall") return item;
|
|
1381
|
+
const existing = this.toolParts.get(item.id);
|
|
1382
|
+
if (existing) {
|
|
1383
|
+
existing.part.args = item.arguments;
|
|
1384
|
+
existing.message = chat;
|
|
1385
|
+
return existing.part;
|
|
1386
|
+
}
|
|
1387
|
+
const part = {
|
|
1388
|
+
type: "toolCall",
|
|
1389
|
+
toolCallId: item.id,
|
|
1390
|
+
toolName: item.name,
|
|
1391
|
+
args: item.arguments,
|
|
1392
|
+
status: "pending"
|
|
1393
|
+
};
|
|
1394
|
+
this.toolParts.set(item.id, {
|
|
1395
|
+
part,
|
|
1396
|
+
message: chat
|
|
1397
|
+
});
|
|
1398
|
+
return part;
|
|
1399
|
+
});
|
|
1400
|
+
if (message.error) chat.error = message.error;
|
|
1401
|
+
}
|
|
1402
|
+
applyToolResult(toolCallId, output, isError, durationMs, notify) {
|
|
1403
|
+
const entry = this.toolParts.get(toolCallId);
|
|
1404
|
+
if (!entry) return;
|
|
1405
|
+
entry.part.status = isError ? "error" : "done";
|
|
1406
|
+
entry.part.output = output ?? entry.part.output;
|
|
1407
|
+
entry.part.durationMs = durationMs ?? entry.part.durationMs;
|
|
1408
|
+
if (notify) this.onMessageChanged(entry.message);
|
|
1409
|
+
}
|
|
1410
|
+
updateTool(toolCallId, update) {
|
|
1411
|
+
const entry = this.toolParts.get(toolCallId);
|
|
1412
|
+
if (!entry) return;
|
|
1413
|
+
update(entry.part);
|
|
1414
|
+
this.onMessageChanged(entry.message);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/main/services/agent/AgentHost.ts
|
|
1419
|
+
/**
|
|
1420
|
+
* Exposes an agent through a small, serializable API, whichever harness runs
|
|
1421
|
+
* it. Owns the state, the transcript and the tools Nubase extensions
|
|
1422
|
+
* contribute; the harness only drives the agent loop.
|
|
1423
|
+
*/
|
|
1424
|
+
var AgentHost = class {
|
|
1425
|
+
harness;
|
|
1426
|
+
transcript;
|
|
1427
|
+
tools = /* @__PURE__ */ new Map();
|
|
1428
|
+
toolsDirty = false;
|
|
1429
|
+
sessionOpen = false;
|
|
1430
|
+
state;
|
|
1431
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
1432
|
+
flushTimer;
|
|
1433
|
+
constructor(options) {
|
|
1434
|
+
this.options = options;
|
|
1435
|
+
this.state = {
|
|
1436
|
+
status: "starting",
|
|
1437
|
+
cwd: options.cwd
|
|
1438
|
+
};
|
|
1439
|
+
this.transcript = new Transcript((message) => this.queueMessage(message));
|
|
1440
|
+
this.harness = options.createHarness((event) => this.handleHarnessEvent(event));
|
|
1441
|
+
}
|
|
1442
|
+
async start() {
|
|
1443
|
+
try {
|
|
1444
|
+
this.sessionOpened(await this.harness.start());
|
|
1445
|
+
await this.applyPendingToolChanges();
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
this.setState({
|
|
1448
|
+
status: "error",
|
|
1449
|
+
error: errorMessage(error)
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
getState() {
|
|
1454
|
+
return this.state;
|
|
1455
|
+
}
|
|
1456
|
+
getMessages() {
|
|
1457
|
+
return this.transcript.all;
|
|
1458
|
+
}
|
|
1459
|
+
registerTool(owner, tool) {
|
|
1460
|
+
const entry = {
|
|
1461
|
+
owner,
|
|
1462
|
+
tool
|
|
1463
|
+
};
|
|
1464
|
+
this.tools.set(tool.name, entry);
|
|
1465
|
+
this.markToolsDirty();
|
|
1466
|
+
return { dispose: () => {
|
|
1467
|
+
if (this.tools.get(tool.name) !== entry) return;
|
|
1468
|
+
this.tools.delete(tool.name);
|
|
1469
|
+
this.markToolsDirty();
|
|
1470
|
+
} };
|
|
1471
|
+
}
|
|
1472
|
+
async prompt(text, images) {
|
|
1473
|
+
this.requireSession();
|
|
1474
|
+
if (this.harness.isRunning()) {
|
|
1475
|
+
await this.ready(() => this.harness.send(text, images, "followUp"));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
await this.applyPendingToolChanges();
|
|
1479
|
+
this.harness.send(text, images, "start").catch((error) => {
|
|
1480
|
+
this.options.onEvent({
|
|
1481
|
+
type: "notification",
|
|
1482
|
+
message: errorMessage(error),
|
|
1483
|
+
level: "error"
|
|
1484
|
+
});
|
|
1485
|
+
this.setState({ status: "idle" });
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
async steer(text, images) {
|
|
1489
|
+
this.requireSession();
|
|
1490
|
+
if (this.harness.isRunning()) await this.ready(() => this.harness.send(text, images, "steer"));
|
|
1491
|
+
else await this.prompt(text, images);
|
|
1492
|
+
}
|
|
1493
|
+
async abort() {
|
|
1494
|
+
await this.harness.abort();
|
|
1495
|
+
}
|
|
1496
|
+
async newSession() {
|
|
1497
|
+
await this.openSession({ kind: "new" });
|
|
1498
|
+
}
|
|
1499
|
+
async switchSession(path) {
|
|
1500
|
+
await this.openSession({
|
|
1501
|
+
kind: "open",
|
|
1502
|
+
path
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
listSessions() {
|
|
1506
|
+
return this.harness.listSessions();
|
|
1507
|
+
}
|
|
1508
|
+
listModels() {
|
|
1509
|
+
return this.harness.listModels();
|
|
1510
|
+
}
|
|
1511
|
+
async setModel(provider, id) {
|
|
1512
|
+
await this.ready(() => this.harness.setModel(provider, id));
|
|
1513
|
+
this.syncState();
|
|
1514
|
+
}
|
|
1515
|
+
listProviders() {
|
|
1516
|
+
return this.harness.listProviders();
|
|
1517
|
+
}
|
|
1518
|
+
async login(providerId, method) {
|
|
1519
|
+
await this.ready(() => this.harness.login(providerId, method));
|
|
1520
|
+
if (!this.sessionOpen) await this.start();
|
|
1521
|
+
this.syncState();
|
|
1522
|
+
}
|
|
1523
|
+
async logout(providerId) {
|
|
1524
|
+
await this.harness.logout(providerId);
|
|
1525
|
+
this.syncState();
|
|
1526
|
+
}
|
|
1527
|
+
dispose() {
|
|
1528
|
+
this.harness.dispose();
|
|
1529
|
+
}
|
|
1530
|
+
handleHarnessEvent(event) {
|
|
1531
|
+
this.transcript.apply(event);
|
|
1532
|
+
switch (event.type) {
|
|
1533
|
+
case "run_start":
|
|
1534
|
+
this.setState({ status: "running" });
|
|
1535
|
+
break;
|
|
1536
|
+
case "run_settled":
|
|
1537
|
+
this.setState({ status: "idle" });
|
|
1538
|
+
this.applyPendingToolChanges();
|
|
1539
|
+
break;
|
|
1540
|
+
case "notice":
|
|
1541
|
+
this.options.onEvent({
|
|
1542
|
+
type: "notification",
|
|
1543
|
+
message: event.message,
|
|
1544
|
+
level: event.level
|
|
1545
|
+
});
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async openSession(ref) {
|
|
1550
|
+
this.sessionOpened(await this.harness.openSession(ref));
|
|
1551
|
+
await this.applyPendingToolChanges();
|
|
1552
|
+
}
|
|
1553
|
+
sessionOpened(history) {
|
|
1554
|
+
this.sessionOpen = true;
|
|
1555
|
+
this.transcript.reset(history);
|
|
1556
|
+
this.options.onEvent({
|
|
1557
|
+
type: "messages",
|
|
1558
|
+
messages: this.transcript.all
|
|
1559
|
+
});
|
|
1560
|
+
this.syncState("idle");
|
|
1561
|
+
}
|
|
1562
|
+
markToolsDirty() {
|
|
1563
|
+
this.toolsDirty = true;
|
|
1564
|
+
if (this.sessionOpen && !this.harness.isRunning()) this.applyPendingToolChanges();
|
|
1565
|
+
}
|
|
1566
|
+
/** Changing tools mid-run would disturb the agent, so changes wait for idle. */
|
|
1567
|
+
async applyPendingToolChanges() {
|
|
1568
|
+
if (!this.toolsDirty || !this.sessionOpen || this.harness.isRunning()) return;
|
|
1569
|
+
this.toolsDirty = false;
|
|
1570
|
+
try {
|
|
1571
|
+
await this.harness.setTools([...this.tools.values()].map((t) => t.tool));
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
this.options.onEvent({
|
|
1574
|
+
type: "notification",
|
|
1575
|
+
message: `Reloading agent tools failed: ${errorMessage(error)}`,
|
|
1576
|
+
level: "error"
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
syncState(status) {
|
|
1581
|
+
const info = this.harness.describe();
|
|
1582
|
+
this.setState({
|
|
1583
|
+
status: status ?? this.state.status,
|
|
1584
|
+
sessionId: info.sessionId,
|
|
1585
|
+
sessionFile: info.sessionFile,
|
|
1586
|
+
thinkingLevel: info.thinkingLevel,
|
|
1587
|
+
model: info.model,
|
|
1588
|
+
error: void 0
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
setState(patch) {
|
|
1592
|
+
this.state = {
|
|
1593
|
+
...this.state,
|
|
1594
|
+
...patch
|
|
1595
|
+
};
|
|
1596
|
+
this.options.onEvent({
|
|
1597
|
+
type: "state",
|
|
1598
|
+
state: this.state
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
queueMessage(message) {
|
|
1602
|
+
this.pendingMessages.set(message.id, message);
|
|
1603
|
+
if (this.flushTimer) return;
|
|
1604
|
+
this.flushTimer = setTimeout(() => {
|
|
1605
|
+
this.flushTimer = void 0;
|
|
1606
|
+
for (const pending of this.pendingMessages.values()) this.options.onEvent({
|
|
1607
|
+
type: "message",
|
|
1608
|
+
message: structuredClone(pending)
|
|
1609
|
+
});
|
|
1610
|
+
this.pendingMessages.clear();
|
|
1611
|
+
}, 33);
|
|
1612
|
+
}
|
|
1613
|
+
requireSession() {
|
|
1614
|
+
if (!this.sessionOpen) throw this.notReady();
|
|
1615
|
+
}
|
|
1616
|
+
/** Reports "not ready" with the startup error, when there is one. */
|
|
1617
|
+
async ready(call) {
|
|
1618
|
+
try {
|
|
1619
|
+
return await call();
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
if (error instanceof HarnessNotReadyError) throw this.notReady();
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
notReady() {
|
|
1626
|
+
return new Error(this.state.error ?? "The agent is still starting");
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1630
|
+
//#endregion
|
|
1631
|
+
//#region src/main/services/agent/harness/pi/piEvents.ts
|
|
1632
|
+
var resultText = (result) => {
|
|
1633
|
+
if (typeof result === "string") return result;
|
|
1634
|
+
const content = result?.content;
|
|
1635
|
+
if (typeof content === "string") return content;
|
|
1636
|
+
if (!Array.isArray(content)) return void 0;
|
|
1637
|
+
return content.map((part) => part.type === "text" ? part.text ?? "" : "[image]").join("\n");
|
|
1638
|
+
};
|
|
1639
|
+
var toContent = (content) => (typeof content === "string" ? [{
|
|
1640
|
+
type: "text",
|
|
1641
|
+
text: content
|
|
1642
|
+
}] : content ?? []).flatMap((item) => {
|
|
1643
|
+
switch (item.type) {
|
|
1644
|
+
case "text": return [{
|
|
1645
|
+
type: "text",
|
|
1646
|
+
text: item.text
|
|
1647
|
+
}];
|
|
1648
|
+
case "thinking": return [{
|
|
1649
|
+
type: "thinking",
|
|
1650
|
+
text: item.thinking
|
|
1651
|
+
}];
|
|
1652
|
+
case "image": return [{
|
|
1653
|
+
type: "image",
|
|
1654
|
+
data: item.data,
|
|
1655
|
+
mimeType: item.mimeType
|
|
1656
|
+
}];
|
|
1657
|
+
case "toolCall": return [{
|
|
1658
|
+
type: "toolCall",
|
|
1659
|
+
id: item.id,
|
|
1660
|
+
name: item.name,
|
|
1661
|
+
arguments: item.arguments
|
|
1662
|
+
}];
|
|
1663
|
+
default: return [];
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
/** Pi's custom roles (bash executions, compaction summaries, …) are not shown. */
|
|
1667
|
+
var toNeutralMessage = (message) => {
|
|
1668
|
+
if (message.role === "toolResult") {
|
|
1669
|
+
if (!message.toolCallId) return void 0;
|
|
1670
|
+
return {
|
|
1671
|
+
role: "toolResult",
|
|
1672
|
+
toolCallId: message.toolCallId,
|
|
1673
|
+
output: resultText(message),
|
|
1674
|
+
isError: message.isError
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (message.role !== "user" && message.role !== "assistant") return void 0;
|
|
1678
|
+
return {
|
|
1679
|
+
role: message.role,
|
|
1680
|
+
content: toContent(message.content),
|
|
1681
|
+
timestamp: message.timestamp,
|
|
1682
|
+
error: message.stopReason === "error" && message.errorMessage ? message.errorMessage : void 0
|
|
1683
|
+
};
|
|
1684
|
+
};
|
|
1685
|
+
/** Translates one pi AgentSessionEvent; returns undefined for events the UI ignores. */
|
|
1686
|
+
var toHarnessEvent = (event) => {
|
|
1687
|
+
switch (event.type) {
|
|
1688
|
+
case "agent_start": return { type: "run_start" };
|
|
1689
|
+
case "agent_settled": return { type: "run_settled" };
|
|
1690
|
+
case "auto_retry_start": return {
|
|
1691
|
+
type: "notice",
|
|
1692
|
+
message: `Retrying (${event.attempt}/${event.maxAttempts}): ${event.errorMessage}`,
|
|
1693
|
+
level: "warning"
|
|
1694
|
+
};
|
|
1695
|
+
case "message_start":
|
|
1696
|
+
case "message_update":
|
|
1697
|
+
case "message_end": {
|
|
1698
|
+
const message = toNeutralMessage(event.message);
|
|
1699
|
+
if (!message || message.role === "toolResult") return void 0;
|
|
1700
|
+
return {
|
|
1701
|
+
type: event.type === "message_start" ? "message_start" : "message_update",
|
|
1702
|
+
message
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
case "tool_execution_start": return {
|
|
1706
|
+
type: "tool_start",
|
|
1707
|
+
toolCallId: event.toolCallId,
|
|
1708
|
+
args: event.args
|
|
1709
|
+
};
|
|
1710
|
+
case "tool_execution_update": return {
|
|
1711
|
+
type: "tool_update",
|
|
1712
|
+
toolCallId: event.toolCallId,
|
|
1713
|
+
output: resultText(event.partialResult)
|
|
1714
|
+
};
|
|
1715
|
+
case "tool_execution_end": return {
|
|
1716
|
+
type: "tool_end",
|
|
1717
|
+
toolCallId: event.toolCallId,
|
|
1718
|
+
output: resultText(event.result),
|
|
1719
|
+
isError: Boolean(event.isError),
|
|
1720
|
+
durationMs: event.durationMs
|
|
1721
|
+
};
|
|
1722
|
+
default: return;
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
//#endregion
|
|
1726
|
+
//#region src/main/services/agent/harness/pi/PiHarness.ts
|
|
1727
|
+
/**
|
|
1728
|
+
* Runs the pi agent in-process. Tools contributed by Nubase are bridged into
|
|
1729
|
+
* pi through one inline pi extension that is re-run whenever they change.
|
|
1730
|
+
*/
|
|
1731
|
+
var PiHarness = class {
|
|
1732
|
+
id = "pi";
|
|
1733
|
+
capabilities = {
|
|
1734
|
+
steer: true,
|
|
1735
|
+
multiProvider: true,
|
|
1736
|
+
accountLogin: true
|
|
1737
|
+
};
|
|
1738
|
+
session;
|
|
1739
|
+
modelRuntime;
|
|
1740
|
+
settingsManager;
|
|
1741
|
+
resourceLoader;
|
|
1742
|
+
unsubscribe;
|
|
1743
|
+
tools = [];
|
|
1744
|
+
constructor(options) {
|
|
1745
|
+
this.options = options;
|
|
1746
|
+
mkdirSync(options.agentDir, { recursive: true });
|
|
1747
|
+
}
|
|
1748
|
+
get sessionsDir() {
|
|
1749
|
+
return join(this.options.agentDir, "sessions");
|
|
1750
|
+
}
|
|
1751
|
+
async start() {
|
|
1752
|
+
const { agentDir, cwd } = this.options;
|
|
1753
|
+
this.modelRuntime = await ModelRuntime.create({
|
|
1754
|
+
authPath: join(agentDir, "auth.json"),
|
|
1755
|
+
modelsPath: join(agentDir, "models.json")
|
|
1756
|
+
});
|
|
1757
|
+
this.settingsManager = SettingsManager.create(cwd, agentDir);
|
|
1758
|
+
this.resourceLoader = new DefaultResourceLoader({
|
|
1759
|
+
cwd,
|
|
1760
|
+
agentDir,
|
|
1761
|
+
settingsManager: this.settingsManager,
|
|
1762
|
+
additionalSkillPaths: this.options.skillPaths,
|
|
1763
|
+
extensionFactories: [{
|
|
1764
|
+
name: "nubase",
|
|
1765
|
+
factory: this.bridgeExtension
|
|
1766
|
+
}],
|
|
1767
|
+
appendSystemPrompt: [this.options.appendSystemPrompt]
|
|
1768
|
+
});
|
|
1769
|
+
await this.resourceLoader.reload();
|
|
1770
|
+
return this.openSession({ kind: "recent" });
|
|
1771
|
+
}
|
|
1772
|
+
async openSession(ref) {
|
|
1773
|
+
const { cwd } = this.options;
|
|
1774
|
+
const sessionManager = ref.kind === "recent" ? SessionManager.continueRecent(cwd, this.sessionsDir) : ref.kind === "new" ? SessionManager.create(cwd, this.sessionsDir) : SessionManager.open(ref.path, this.sessionsDir);
|
|
1775
|
+
return this.openWith(sessionManager);
|
|
1776
|
+
}
|
|
1777
|
+
async send(text, images, mode) {
|
|
1778
|
+
const session = this.requireSession();
|
|
1779
|
+
const imageContent = images?.length ? images.map((image) => ({
|
|
1780
|
+
type: "image",
|
|
1781
|
+
data: image.data,
|
|
1782
|
+
mimeType: image.mimeType
|
|
1783
|
+
})) : void 0;
|
|
1784
|
+
await session.prompt(text, {
|
|
1785
|
+
images: imageContent,
|
|
1786
|
+
...mode === "start" ? {} : { streamingBehavior: mode }
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
async abort() {
|
|
1790
|
+
await this.session?.abort();
|
|
1791
|
+
}
|
|
1792
|
+
isRunning() {
|
|
1793
|
+
return Boolean(this.session?.isStreaming);
|
|
1794
|
+
}
|
|
1795
|
+
async setTools(tools) {
|
|
1796
|
+
this.tools = tools;
|
|
1797
|
+
await this.session?.reload();
|
|
1798
|
+
}
|
|
1799
|
+
describe() {
|
|
1800
|
+
const session = this.session;
|
|
1801
|
+
const model = this.hasUsableModel() ? session?.model : void 0;
|
|
1802
|
+
return {
|
|
1803
|
+
sessionId: session?.sessionId,
|
|
1804
|
+
sessionFile: session?.sessionFile,
|
|
1805
|
+
thinkingLevel: session?.thinkingLevel,
|
|
1806
|
+
model: model ? {
|
|
1807
|
+
provider: model.provider,
|
|
1808
|
+
id: model.id,
|
|
1809
|
+
name: model.name ?? model.id
|
|
1810
|
+
} : void 0
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
async listSessions() {
|
|
1814
|
+
return (await SessionManager.list(this.options.cwd, this.sessionsDir)).map((s) => ({
|
|
1815
|
+
id: s.id,
|
|
1816
|
+
path: s.path,
|
|
1817
|
+
name: s.name,
|
|
1818
|
+
firstMessage: s.firstMessage,
|
|
1819
|
+
modified: s.modified.getTime(),
|
|
1820
|
+
messageCount: s.messageCount
|
|
1821
|
+
})).sort((a, b) => b.modified - a.modified);
|
|
1822
|
+
}
|
|
1823
|
+
async listModels() {
|
|
1824
|
+
if (!this.modelRuntime) return [];
|
|
1825
|
+
return (await this.modelRuntime.getAvailable()).map((m) => ({
|
|
1826
|
+
provider: m.provider,
|
|
1827
|
+
id: m.id,
|
|
1828
|
+
name: m.name ?? m.id
|
|
1829
|
+
}));
|
|
1830
|
+
}
|
|
1831
|
+
async setModel(provider, id) {
|
|
1832
|
+
const model = this.modelRuntime?.getModel(provider, id);
|
|
1833
|
+
if (!model) throw new Error(`Unknown model ${provider}/${id}`);
|
|
1834
|
+
await this.requireSession().setModel(model);
|
|
1835
|
+
}
|
|
1836
|
+
/** Providers the user can connect, with how they sign in and whether they are ready. */
|
|
1837
|
+
listProviders() {
|
|
1838
|
+
const runtime = this.modelRuntime;
|
|
1839
|
+
if (!runtime) return [];
|
|
1840
|
+
return runtime.getProviders().map((provider) => {
|
|
1841
|
+
const status = runtime.getProviderAuthStatus(provider.id);
|
|
1842
|
+
return {
|
|
1843
|
+
id: provider.id,
|
|
1844
|
+
name: provider.name,
|
|
1845
|
+
configured: status.configured,
|
|
1846
|
+
source: status.source,
|
|
1847
|
+
accountLogin: provider.auth.oauth ? {
|
|
1848
|
+
name: provider.auth.oauth.name,
|
|
1849
|
+
subscription: Boolean(provider.auth.oauth.isSubscription)
|
|
1850
|
+
} : void 0,
|
|
1851
|
+
apiKeyLogin: Boolean(provider.auth.apiKey?.login)
|
|
1852
|
+
};
|
|
1853
|
+
}).filter((p) => p.accountLogin || p.apiKeyLogin || p.configured).sort((a, b) => a.name.localeCompare(b.name));
|
|
1854
|
+
}
|
|
1855
|
+
/** Runs pi's own sign-in flow for a provider, with Nubase dialogs as the UI. */
|
|
1856
|
+
async login(providerId, method) {
|
|
1857
|
+
const runtime = this.modelRuntime;
|
|
1858
|
+
if (!runtime) throw new HarnessNotReadyError();
|
|
1859
|
+
await runtime.login(providerId, method === "account" ? "oauth" : "api_key", this.createLoginInteraction());
|
|
1860
|
+
await runtime.refresh({ providers: [providerId] }).catch(() => void 0);
|
|
1861
|
+
await this.ensureUsableModel();
|
|
1862
|
+
}
|
|
1863
|
+
async logout(providerId) {
|
|
1864
|
+
await this.modelRuntime?.logout(providerId);
|
|
1865
|
+
}
|
|
1866
|
+
dispose() {
|
|
1867
|
+
this.unsubscribe?.();
|
|
1868
|
+
this.session?.dispose();
|
|
1869
|
+
}
|
|
1870
|
+
async openWith(sessionManager) {
|
|
1871
|
+
if (!this.modelRuntime || !this.resourceLoader || !this.settingsManager) throw new Error("Agent is not initialized");
|
|
1872
|
+
this.unsubscribe?.();
|
|
1873
|
+
this.session?.dispose();
|
|
1874
|
+
const preferred = this.options.model ? this.modelRuntime.getModel(this.options.model.provider, this.options.model.id) : void 0;
|
|
1875
|
+
const { session } = await createAgentSession({
|
|
1876
|
+
cwd: this.options.cwd,
|
|
1877
|
+
agentDir: this.options.agentDir,
|
|
1878
|
+
modelRuntime: this.modelRuntime,
|
|
1879
|
+
resourceLoader: this.resourceLoader,
|
|
1880
|
+
settingsManager: this.settingsManager,
|
|
1881
|
+
sessionManager,
|
|
1882
|
+
...preferred ? { model: preferred } : {}
|
|
1883
|
+
});
|
|
1884
|
+
this.session = session;
|
|
1885
|
+
await session.bindExtensions({
|
|
1886
|
+
mode: "rpc",
|
|
1887
|
+
uiContext: this.createUiContext()
|
|
1888
|
+
});
|
|
1889
|
+
this.unsubscribe = session.subscribe((event) => {
|
|
1890
|
+
const translated = toHarnessEvent(event);
|
|
1891
|
+
if (translated) this.options.emit(translated);
|
|
1892
|
+
});
|
|
1893
|
+
await this.ensureUsableModel();
|
|
1894
|
+
return session.messages.flatMap((message) => {
|
|
1895
|
+
const neutral = toNeutralMessage(message);
|
|
1896
|
+
return neutral ? [neutral] : [];
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
bridgeExtension = (pi) => {
|
|
1900
|
+
for (const tool of this.tools) pi.registerTool({
|
|
1901
|
+
name: tool.name,
|
|
1902
|
+
label: tool.label ?? tool.name,
|
|
1903
|
+
description: tool.description,
|
|
1904
|
+
parameters: tool.parameters,
|
|
1905
|
+
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
1906
|
+
const raw = await tool.execute(params, {
|
|
1907
|
+
signal,
|
|
1908
|
+
update: (text) => onUpdate?.({
|
|
1909
|
+
content: [{
|
|
1910
|
+
type: "text",
|
|
1911
|
+
text
|
|
1912
|
+
}],
|
|
1913
|
+
details: void 0
|
|
1914
|
+
})
|
|
1915
|
+
});
|
|
1916
|
+
const result = typeof raw === "string" ? { text: raw } : raw;
|
|
1917
|
+
if (result.isError) throw new Error(result.text);
|
|
1918
|
+
return {
|
|
1919
|
+
content: [{
|
|
1920
|
+
type: "text",
|
|
1921
|
+
text: result.text
|
|
1922
|
+
}, ...(result.images ?? []).map((image) => ({
|
|
1923
|
+
type: "image",
|
|
1924
|
+
data: image.data,
|
|
1925
|
+
mimeType: image.mimeType
|
|
1926
|
+
}))],
|
|
1927
|
+
details: result.details ?? {}
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
};
|
|
1932
|
+
createLoginInteraction() {
|
|
1933
|
+
const { dialogs } = this.options;
|
|
1934
|
+
const cancelled = () => /* @__PURE__ */ new Error("Login cancelled");
|
|
1935
|
+
return {
|
|
1936
|
+
prompt: async (prompt) => {
|
|
1937
|
+
if (prompt.type === "select") {
|
|
1938
|
+
const labels = prompt.options.map((o) => o.description ? `${o.label} — ${o.description}` : o.label);
|
|
1939
|
+
const choice = await dialogs.select(prompt.message, labels);
|
|
1940
|
+
const option = prompt.options[labels.indexOf(choice ?? "")];
|
|
1941
|
+
if (!option) throw cancelled();
|
|
1942
|
+
return option.id;
|
|
1943
|
+
}
|
|
1944
|
+
const value = await dialogs.input(prompt.message, {
|
|
1945
|
+
placeholder: prompt.placeholder,
|
|
1946
|
+
secret: prompt.type === "secret",
|
|
1947
|
+
signal: prompt.signal
|
|
1948
|
+
});
|
|
1949
|
+
if (value === void 0) throw cancelled();
|
|
1950
|
+
return value;
|
|
1951
|
+
},
|
|
1952
|
+
notify: (event) => {
|
|
1953
|
+
switch (event.type) {
|
|
1954
|
+
case "auth_url":
|
|
1955
|
+
dialogs.openExternal(event.url);
|
|
1956
|
+
dialogs.notify(event.instructions ?? "Continue signing in in your browser.", "info");
|
|
1957
|
+
break;
|
|
1958
|
+
case "device_code":
|
|
1959
|
+
dialogs.openExternal(event.verificationUri);
|
|
1960
|
+
dialogs.confirm(`Enter this code in your browser: ${event.userCode}`, `If the page did not open, go to ${event.verificationUri}`);
|
|
1961
|
+
break;
|
|
1962
|
+
case "info":
|
|
1963
|
+
case "progress":
|
|
1964
|
+
dialogs.notify(event.message, "info");
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
/** pi extension UI requests, answered with Nubase dialogs. */
|
|
1971
|
+
createUiContext() {
|
|
1972
|
+
const { dialogs } = this.options;
|
|
1973
|
+
return new Proxy({
|
|
1974
|
+
select: (title, options) => dialogs.select(title, options),
|
|
1975
|
+
confirm: (title, message) => dialogs.confirm(title, message),
|
|
1976
|
+
input: (title, placeholder) => dialogs.input(title, { placeholder }),
|
|
1977
|
+
notify: (message, type) => dialogs.notify(message, type === "error" ? "error" : "info"),
|
|
1978
|
+
editor: (title, prefill) => dialogs.input(title, { placeholder: prefill })
|
|
1979
|
+
}, { get: (target, prop) => prop in target ? target[prop] : () => prop === "onTerminalInput" ? () => {} : void 0 });
|
|
1980
|
+
}
|
|
1981
|
+
/** pi defaults to a model even without credentials; prefer one that can actually run. */
|
|
1982
|
+
async ensureUsableModel() {
|
|
1983
|
+
const session = this.session;
|
|
1984
|
+
const runtime = this.modelRuntime;
|
|
1985
|
+
if (!session || !runtime) return;
|
|
1986
|
+
const current = session.model;
|
|
1987
|
+
if (current && runtime.hasConfiguredAuth(current.provider)) return;
|
|
1988
|
+
const fallback = (await runtime.getAvailable().catch(() => []))[0];
|
|
1989
|
+
if (fallback) await session.setModel(fallback);
|
|
1990
|
+
}
|
|
1991
|
+
hasUsableModel() {
|
|
1992
|
+
const model = this.session?.model;
|
|
1993
|
+
return Boolean(model && this.modelRuntime?.hasConfiguredAuth(model.provider));
|
|
1994
|
+
}
|
|
1995
|
+
requireSession() {
|
|
1996
|
+
if (!this.session) throw new HarnessNotReadyError();
|
|
1997
|
+
return this.session;
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
//#endregion
|
|
2001
|
+
//#region src/main/services/agent/nubaseTools.ts
|
|
2002
|
+
var formatDiagnostics = (diagnostics) => diagnostics.map((d) => {
|
|
2003
|
+
const where = d.file ? `${d.file}${d.line ? `:${d.line}:${d.column ?? 1}` : ""} ` : "";
|
|
2004
|
+
return `- [${d.source}] ${where}${d.message}`;
|
|
2005
|
+
}).join("\n");
|
|
2006
|
+
var describe = (info) => `${info.id} (${info.displayName}) — ${info.status}${info.builtin ? ", built-in" : `, ${info.source}, ${info.path}`}`;
|
|
2007
|
+
var object = (properties, required = []) => ({
|
|
2008
|
+
type: "object",
|
|
2009
|
+
properties,
|
|
2010
|
+
required,
|
|
2011
|
+
additionalProperties: false
|
|
2012
|
+
});
|
|
2013
|
+
/**
|
|
2014
|
+
* Tools that let the agent change Nubase itself: create and build
|
|
2015
|
+
* extensions, arrange the dock, run UI commands, and look at the result.
|
|
2016
|
+
*/
|
|
2017
|
+
var createNubaseTools = (deps) => {
|
|
2018
|
+
const { server, extensions } = deps;
|
|
2019
|
+
const buildAndLoad = async (id) => {
|
|
2020
|
+
const info = await extensions.rebuild(id);
|
|
2021
|
+
if (info.status !== "active") return [
|
|
2022
|
+
`Build of "${id}" FAILED (${info.status}). The previous working version (if any) is still loaded.`,
|
|
2023
|
+
formatDiagnostics(info.diagnostics),
|
|
2024
|
+
"Fix these errors and call nubase_build_extension again."
|
|
2025
|
+
].join("\n");
|
|
2026
|
+
const lines = [`Built "${id}" successfully (build ${info.buildId}).`];
|
|
2027
|
+
if (info.buildId && info.uiBundleUrl) {
|
|
2028
|
+
const loaded = await server.requestRenderer("extensions.ensureLoaded", id, info.buildId);
|
|
2029
|
+
if (!loaded.ok) {
|
|
2030
|
+
lines.push(`But loading it in the UI failed:\n${loaded.error}`);
|
|
2031
|
+
return lines.join("\n");
|
|
2032
|
+
}
|
|
2033
|
+
lines.push(`Loaded in the UI. Widgets: ${loaded.widgets.join(", ") || "(none)"}. Commands: ${loaded.commands.join(", ") || "(none)"}.`);
|
|
2034
|
+
}
|
|
2035
|
+
return lines.join("\n");
|
|
2036
|
+
};
|
|
2037
|
+
return [
|
|
2038
|
+
{
|
|
2039
|
+
name: "nubase_list_extensions",
|
|
2040
|
+
label: "List extensions",
|
|
2041
|
+
description: "Lists Nubase extensions (built-in, global and folder-specific) with their status. User extensions are plain folders you can edit with the regular file tools. A folder extension with the same id as a global one replaces it in this folder.",
|
|
2042
|
+
parameters: object({}),
|
|
2043
|
+
execute: () => [
|
|
2044
|
+
`Global extensions folder: ${deps.globalExtensionsDir}`,
|
|
2045
|
+
deps.folderExtensionsDir ? `This folder's extensions folder: ${deps.folderExtensionsDir}` : "This is the ~ tab: only global extensions apply.",
|
|
2046
|
+
...extensions.list().map((info) => describe(info) + (info.diagnostics.length ? `\n${formatDiagnostics(info.diagnostics)}` : ""))
|
|
2047
|
+
].join("\n")
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
name: "nubase_create_extension",
|
|
2051
|
+
label: "Create extension",
|
|
2052
|
+
description: "Scaffolds a new Nubase extension (a TypeScript project with a widget) and builds it. By default it is global (available in every folder). Use scope \"folder\" only when the user asks for something specific to the current folder/project. Read the nubase-extension-authoring skill before editing it. Use withHost when the extension needs Node access: secrets, HTTP calls without CORS, or agent tools.",
|
|
2053
|
+
parameters: object({
|
|
2054
|
+
id: {
|
|
2055
|
+
type: "string",
|
|
2056
|
+
description: "lowercase kebab-case, e.g. jira-tickets"
|
|
2057
|
+
},
|
|
2058
|
+
displayName: { type: "string" },
|
|
2059
|
+
description: { type: "string" },
|
|
2060
|
+
withHost: {
|
|
2061
|
+
type: "boolean",
|
|
2062
|
+
description: "Also create src/host.ts (Node side)."
|
|
2063
|
+
},
|
|
2064
|
+
scope: {
|
|
2065
|
+
type: "string",
|
|
2066
|
+
enum: ["global", "folder"],
|
|
2067
|
+
description: "global (default): ~/.nubase/extensions. folder: <folder>/.nubase/extensions, only for this folder."
|
|
2068
|
+
}
|
|
2069
|
+
}, ["id", "displayName"]),
|
|
2070
|
+
execute: async (params) => {
|
|
2071
|
+
const { scope = "global", ...options } = params;
|
|
2072
|
+
if (scope === "folder" && !deps.folderExtensionsDir) return {
|
|
2073
|
+
text: "The ~ tab has no folder of its own; use scope \"global\".",
|
|
2074
|
+
isError: true
|
|
2075
|
+
};
|
|
2076
|
+
const path = await scaffoldExtension(scope === "folder" ? await deps.ensureFolderExtensions() : deps.globalExtensionsDir, deps.templateDir, {
|
|
2077
|
+
...options,
|
|
2078
|
+
withHost: options.withHost ?? false
|
|
2079
|
+
});
|
|
2080
|
+
const result = await buildAndLoad(params.id);
|
|
2081
|
+
return [
|
|
2082
|
+
`Created ${path}`,
|
|
2083
|
+
`Files: package.json, tsconfig.json, src/ui.tsx${params.withHost ? ", src/host.ts" : ""}`,
|
|
2084
|
+
`The starter widget id is "${params.id}.main". Edit the files, then call nubase_build_extension.`,
|
|
2085
|
+
result
|
|
2086
|
+
].join("\n");
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
name: "nubase_build_extension",
|
|
2091
|
+
label: "Build extension",
|
|
2092
|
+
description: "Builds an extension (bundle + typecheck + styles) and hot-reloads it in the running UI. Returns errors to fix, or the widgets and commands it registered. Saving files also triggers a rebuild, but call this to get the result.",
|
|
2093
|
+
parameters: object({ id: { type: "string" } }, ["id"]),
|
|
2094
|
+
execute: async ({ id }) => buildAndLoad(id)
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
name: "nubase_revert_extension",
|
|
2098
|
+
label: "Revert extension",
|
|
2099
|
+
description: "Undoes the latest change to an extension and reloads it: discards unsaved edits, or (if none) steps back one successful build. Call repeatedly to go further back. Every successful build is committed to git.",
|
|
2100
|
+
parameters: object({ id: { type: "string" } }, ["id"]),
|
|
2101
|
+
execute: async ({ id }) => `Reverted "${id}" to: ${await extensions.revert(id)}`
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
name: "nubase_extension_logs",
|
|
2105
|
+
label: "Extension logs",
|
|
2106
|
+
description: "Returns recent runtime errors and logs from extensions (render errors caught by widget error boundaries, console errors, host-side logs).",
|
|
2107
|
+
parameters: object({ id: {
|
|
2108
|
+
type: "string",
|
|
2109
|
+
description: "Omit for all extensions."
|
|
2110
|
+
} }),
|
|
2111
|
+
execute: ({ id }) => {
|
|
2112
|
+
const logs = extensions.getLogs(id).slice(-40);
|
|
2113
|
+
if (logs.length === 0) return "No logs.";
|
|
2114
|
+
return logs.map((l) => `${new Date(l.timestamp).toISOString()} [${l.level}] ${l.extensionId ?? "?"}${l.blockId ? ` block=${l.blockId}` : ""}: ${l.message}${l.stack ? `\n${l.stack.split("\n").slice(0, 6).join("\n")}` : ""}`).join("\n");
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
name: "nubase_get_layout",
|
|
2119
|
+
label: "Get layout",
|
|
2120
|
+
description: "Returns the dock layout (tabs, splits and blocks with their widget ids) and the widgets that can be opened.",
|
|
2121
|
+
parameters: object({}),
|
|
2122
|
+
execute: async () => {
|
|
2123
|
+
const [layout, widgets] = await Promise.all([server.requestRenderer("layout.get"), server.requestRenderer("widgets.list")]);
|
|
2124
|
+
return [
|
|
2125
|
+
"Layout:",
|
|
2126
|
+
JSON.stringify(layout, null, 2),
|
|
2127
|
+
"Available widgets:",
|
|
2128
|
+
...widgets.map((w) => `- ${w.id}: ${w.title}${w.description ? ` — ${w.description}` : ""} (${w.extensionId})`)
|
|
2129
|
+
].join("\n");
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
name: "nubase_open_widget",
|
|
2134
|
+
label: "Open widget",
|
|
2135
|
+
description: "Opens a widget in a new dock block next to the focused one (or relative to a given block). Returns the block id.",
|
|
2136
|
+
parameters: object({
|
|
2137
|
+
widgetId: { type: "string" },
|
|
2138
|
+
placement: {
|
|
2139
|
+
type: "string",
|
|
2140
|
+
enum: [
|
|
2141
|
+
"right",
|
|
2142
|
+
"below",
|
|
2143
|
+
"left",
|
|
2144
|
+
"above",
|
|
2145
|
+
"replace"
|
|
2146
|
+
]
|
|
2147
|
+
},
|
|
2148
|
+
relativeTo: {
|
|
2149
|
+
type: "string",
|
|
2150
|
+
description: "Block id; defaults to the focused block."
|
|
2151
|
+
},
|
|
2152
|
+
state: { description: "Initial widget state." }
|
|
2153
|
+
}, ["widgetId"]),
|
|
2154
|
+
execute: async (params) => {
|
|
2155
|
+
const blockId = await server.requestRenderer("layout.openWidget", params.widgetId, params);
|
|
2156
|
+
return `Opened ${params.widgetId} in block ${blockId}.`;
|
|
2157
|
+
}
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
name: "nubase_close_block",
|
|
2161
|
+
label: "Close block",
|
|
2162
|
+
description: "Closes a dock block by id.",
|
|
2163
|
+
parameters: object({ blockId: { type: "string" } }, ["blockId"]),
|
|
2164
|
+
execute: async ({ blockId }) => {
|
|
2165
|
+
await server.requestRenderer("layout.closeBlock", blockId);
|
|
2166
|
+
return `Closed ${blockId}.`;
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
name: "nubase_list_commands",
|
|
2171
|
+
label: "List commands",
|
|
2172
|
+
description: "Lists UI commands (the same ones in the command palette).",
|
|
2173
|
+
parameters: object({}),
|
|
2174
|
+
execute: async () => (await server.requestRenderer("commands.list")).map((c) => `- ${c.id}: ${c.category ? `${c.category}: ` : ""}${c.title}`).join("\n")
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
name: "nubase_run_command",
|
|
2178
|
+
label: "Run command",
|
|
2179
|
+
description: "Runs a UI command by id, with optional arguments.",
|
|
2180
|
+
parameters: object({
|
|
2181
|
+
id: { type: "string" },
|
|
2182
|
+
args: {}
|
|
2183
|
+
}, ["id"]),
|
|
2184
|
+
execute: async ({ id, args }) => {
|
|
2185
|
+
const result = await server.requestRenderer("commands.execute", id, args);
|
|
2186
|
+
return result === void 0 ? `Ran ${id}.` : JSON.stringify(result);
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
name: "nubase_screenshot",
|
|
2191
|
+
label: "Screenshot",
|
|
2192
|
+
description: "Takes a screenshot of one dock block (or the whole window) so you can check what a widget looks like. Use after building a widget.",
|
|
2193
|
+
parameters: object({ blockId: {
|
|
2194
|
+
type: "string",
|
|
2195
|
+
description: "Omit for the whole window."
|
|
2196
|
+
} }),
|
|
2197
|
+
execute: async ({ blockId }) => {
|
|
2198
|
+
const contents = deps.getWebContents();
|
|
2199
|
+
if (!contents) return {
|
|
2200
|
+
text: "No window is open.",
|
|
2201
|
+
isError: true
|
|
2202
|
+
};
|
|
2203
|
+
const bounds = blockId ? await server.requestRenderer("block.bounds", blockId) : void 0;
|
|
2204
|
+
if (blockId && !bounds) return {
|
|
2205
|
+
text: `No block ${blockId}.`,
|
|
2206
|
+
isError: true
|
|
2207
|
+
};
|
|
2208
|
+
let image = await contents.capturePage(bounds ? {
|
|
2209
|
+
x: Math.round(bounds.x),
|
|
2210
|
+
y: Math.round(bounds.y),
|
|
2211
|
+
width: Math.round(bounds.width),
|
|
2212
|
+
height: Math.round(bounds.height)
|
|
2213
|
+
} : void 0);
|
|
2214
|
+
if (image.getSize().width > 1400) image = image.resize({ width: 1400 });
|
|
2215
|
+
return {
|
|
2216
|
+
text: `Screenshot of ${blockId ? `block ${blockId}` : "the window"}.`,
|
|
2217
|
+
images: [{
|
|
2218
|
+
data: image.toPNG().toString("base64"),
|
|
2219
|
+
mimeType: "image/png"
|
|
2220
|
+
}]
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
];
|
|
2225
|
+
};
|
|
2226
|
+
//#endregion
|
|
2227
|
+
//#region src/main/services/agent/systemPrompt.ts
|
|
2228
|
+
var folderSection = (options) => options.isHome || !options.folderNubaseDir ? `
|
|
2229
|
+
You are in the ~ tab (the user's home folder). It is configured only by
|
|
2230
|
+
${options.nubaseHome}.` : `
|
|
2231
|
+
You are in the tab for the folder ${options.folder}. Its configuration is
|
|
2232
|
+
${options.nubaseHome} with ${options.folderNubaseDir} layered on top:
|
|
2233
|
+
- ${options.folderNubaseDir}/settings.json overrides keys of
|
|
2234
|
+
${options.nubaseHome}/settings.json for this folder only (e.g. a different
|
|
2235
|
+
theme). Edit the root file for changes meant for every folder.
|
|
2236
|
+
- ${options.folderNubaseDir}/extensions/<id> replaces the global extension with
|
|
2237
|
+
the same id, in this folder only.
|
|
2238
|
+
New extensions are global unless the user asks for one specific to this
|
|
2239
|
+
folder or project (then pass scope "folder" to nubase_create_extension).`;
|
|
2240
|
+
var nubaseSystemPrompt = (options) => `
|
|
2241
|
+
# Nubase
|
|
2242
|
+
|
|
2243
|
+
You are running inside Nubase, a desktop AI harness whose UI you can change.
|
|
2244
|
+
Each top-level tab is a folder; this conversation belongs to one of them.
|
|
2245
|
+
The UI is a dock of blocks; every block shows a widget. Widgets, commands,
|
|
2246
|
+
keybindings and themes come from extensions: TypeScript projects in
|
|
2247
|
+
${options.globalExtensionsDir} (global) or in a folder's .nubase/extensions.
|
|
2248
|
+
Built-in widgets (chat, files, terminal, extensions, settings) use the same
|
|
2249
|
+
public API you do.
|
|
2250
|
+
${folderSection(options)}
|
|
2251
|
+
|
|
2252
|
+
When the user asks for new UI — a widget, a dashboard, an integration with a
|
|
2253
|
+
service — build it as an extension:
|
|
2254
|
+
|
|
2255
|
+
1. Read the nubase-extension-authoring skill before writing extension code.
|
|
2256
|
+
2. Use nubase_create_extension (or edit an existing extension).
|
|
2257
|
+
3. Edit the files with your file tools. Never edit dist/ or tsconfig.base.json.
|
|
2258
|
+
4. Call nubase_build_extension and fix every reported error until it builds.
|
|
2259
|
+
5. Open the widget with nubase_open_widget, then check it with nubase_screenshot.
|
|
2260
|
+
6. If something renders wrong, read nubase_extension_logs and iterate.
|
|
2261
|
+
|
|
2262
|
+
Secrets (API tokens) belong in the extension's host half: call
|
|
2263
|
+
ctx.secrets.prompt(...) there. Never ask the user to paste secrets into chat.
|
|
2264
|
+
Every successful build is committed to git; nubase_revert_extension restores
|
|
2265
|
+
the last version that built.
|
|
2266
|
+
`;
|
|
2267
|
+
//#endregion
|
|
2268
|
+
//#region src/main/services/extensions/ExtensionHost.ts
|
|
2269
|
+
globalThis[HOST_MODULES_GLOBAL] = {
|
|
2270
|
+
"@nubase/sdk/host": sdkHost,
|
|
2271
|
+
"@nubase/sdk": sdkHost
|
|
2272
|
+
};
|
|
2273
|
+
var Scope = class {
|
|
2274
|
+
items = [];
|
|
2275
|
+
add(d) {
|
|
2276
|
+
this.items.push(d);
|
|
2277
|
+
return d;
|
|
2278
|
+
}
|
|
2279
|
+
dispose() {
|
|
2280
|
+
for (const d of this.items.splice(0).reverse()) try {
|
|
2281
|
+
d.dispose();
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
console.error("Error disposing extension resource", error);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
var MAX_LOGS = 200;
|
|
2288
|
+
/**
|
|
2289
|
+
* The extensions one folder tab runs. Builds come from the sources (shared
|
|
2290
|
+
* between tabs); this activates the winning version of each id — the
|
|
2291
|
+
* folder's own over the global one — into this tab's agent and UI.
|
|
2292
|
+
*/
|
|
2293
|
+
var ExtensionHost = class {
|
|
2294
|
+
records = /* @__PURE__ */ new Map();
|
|
2295
|
+
sources;
|
|
2296
|
+
logs = [];
|
|
2297
|
+
unsubscribe = [];
|
|
2298
|
+
syncing = /* @__PURE__ */ new Map();
|
|
2299
|
+
constructor(deps) {
|
|
2300
|
+
this.deps = deps;
|
|
2301
|
+
this.sources = [...deps.sources];
|
|
2302
|
+
}
|
|
2303
|
+
async start() {
|
|
2304
|
+
for (const builtin of this.deps.builtins) {
|
|
2305
|
+
const record = {
|
|
2306
|
+
builtin,
|
|
2307
|
+
rpc: /* @__PURE__ */ new Map(),
|
|
2308
|
+
info: {
|
|
2309
|
+
id: builtin.id,
|
|
2310
|
+
displayName: builtin.displayName,
|
|
2311
|
+
description: builtin.description,
|
|
2312
|
+
path: "",
|
|
2313
|
+
builtin: true,
|
|
2314
|
+
source: "builtin",
|
|
2315
|
+
status: "active",
|
|
2316
|
+
diagnostics: []
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
this.records.set(builtin.id, record);
|
|
2320
|
+
if (builtin.host) await this.activateHost(record, builtin.host);
|
|
2321
|
+
}
|
|
2322
|
+
if (!this.deps.safeMode) {
|
|
2323
|
+
for (const source of this.sources) this.subscribe(source);
|
|
2324
|
+
await Promise.all(this.sources.map((source) => source.start()));
|
|
2325
|
+
await this.syncAll();
|
|
2326
|
+
}
|
|
2327
|
+
this.changed();
|
|
2328
|
+
}
|
|
2329
|
+
/** Adds the folder's own extensions once its `.nubase/extensions` exists. Takes precedence over the others. */
|
|
2330
|
+
async addSource(source) {
|
|
2331
|
+
if (this.sources.includes(source)) return;
|
|
2332
|
+
this.sources.unshift(source);
|
|
2333
|
+
if (this.deps.safeMode) return;
|
|
2334
|
+
this.subscribe(source);
|
|
2335
|
+
await source.start();
|
|
2336
|
+
await this.syncAll();
|
|
2337
|
+
}
|
|
2338
|
+
list() {
|
|
2339
|
+
return [...this.records.values()].map((r) => r.info);
|
|
2340
|
+
}
|
|
2341
|
+
get(id) {
|
|
2342
|
+
return this.records.get(id)?.info;
|
|
2343
|
+
}
|
|
2344
|
+
/** The source an id resolves to (highest precedence that has it). */
|
|
2345
|
+
sourceOf(id) {
|
|
2346
|
+
return this.sources.find((source) => source.contains(id));
|
|
2347
|
+
}
|
|
2348
|
+
pathOf(id) {
|
|
2349
|
+
const source = this.sourceOf(id);
|
|
2350
|
+
if (!source) throw new Error(`No extension "${id}"`);
|
|
2351
|
+
return source.pathOf(id);
|
|
2352
|
+
}
|
|
2353
|
+
/** Builds (or rebuilds) an extension and hot-reloads it if the build succeeds. */
|
|
2354
|
+
async rebuild(id) {
|
|
2355
|
+
const record = this.records.get(id);
|
|
2356
|
+
if (record?.builtin) return record.info;
|
|
2357
|
+
const source = this.sourceOf(id);
|
|
2358
|
+
if (!source) throw new Error(`No extension "${id}" in ${this.sources.map((s) => s.dir).join(" or ")}`);
|
|
2359
|
+
await source.rebuild(id);
|
|
2360
|
+
await this.sync(id);
|
|
2361
|
+
const info = this.records.get(id)?.info;
|
|
2362
|
+
if (!info) throw new Error(`Extension "${id}" disappeared`);
|
|
2363
|
+
return info;
|
|
2364
|
+
}
|
|
2365
|
+
async revert(id) {
|
|
2366
|
+
const source = this.sourceOf(id);
|
|
2367
|
+
if (!source) throw new Error(`No extension "${id}"`);
|
|
2368
|
+
const commit = await source.revert(id);
|
|
2369
|
+
await this.sync(id);
|
|
2370
|
+
return commit;
|
|
2371
|
+
}
|
|
2372
|
+
async setEnabled(id) {
|
|
2373
|
+
await this.sync(id);
|
|
2374
|
+
}
|
|
2375
|
+
/** Re-applies settings such as `disabledExtensions` to every extension. */
|
|
2376
|
+
async refresh() {
|
|
2377
|
+
await this.syncAll();
|
|
2378
|
+
}
|
|
2379
|
+
async call(id, method, args) {
|
|
2380
|
+
const handler = this.records.get(id)?.rpc.get(method);
|
|
2381
|
+
if (!handler) throw new Error(`Extension "${id}" has no host method "${method}"`);
|
|
2382
|
+
return handler(...args);
|
|
2383
|
+
}
|
|
2384
|
+
reportLog(entry) {
|
|
2385
|
+
this.logs.push(entry);
|
|
2386
|
+
if (this.logs.length > MAX_LOGS) this.logs.splice(0, this.logs.length - MAX_LOGS);
|
|
2387
|
+
}
|
|
2388
|
+
getLogs(id, since) {
|
|
2389
|
+
return this.logs.filter((l) => (!id || l.extensionId === id) && (!since || l.timestamp >= since));
|
|
2390
|
+
}
|
|
2391
|
+
dispose() {
|
|
2392
|
+
for (const unsubscribe of this.unsubscribe.splice(0)) unsubscribe();
|
|
2393
|
+
for (const record of this.records.values()) record.hostScope?.dispose();
|
|
2394
|
+
}
|
|
2395
|
+
subscribe(source) {
|
|
2396
|
+
this.unsubscribe.push(source.onChanged((id) => this.sync(id)));
|
|
2397
|
+
}
|
|
2398
|
+
async syncAll() {
|
|
2399
|
+
const ids = new Set(this.sources.flatMap((source) => source.ids()));
|
|
2400
|
+
for (const record of this.records.values()) if (!record.builtin) ids.add(record.info.id);
|
|
2401
|
+
await Promise.all([...ids].map((id) => this.sync(id)));
|
|
2402
|
+
}
|
|
2403
|
+
/** Serializes work per id: activation is async and sources notify often. */
|
|
2404
|
+
sync(id) {
|
|
2405
|
+
const next = (this.syncing.get(id) ?? Promise.resolve()).then(() => this.doSync(id));
|
|
2406
|
+
const tracked = next.finally(() => {
|
|
2407
|
+
if (this.syncing.get(id) === tracked) this.syncing.delete(id);
|
|
2408
|
+
});
|
|
2409
|
+
this.syncing.set(id, tracked);
|
|
2410
|
+
return next;
|
|
2411
|
+
}
|
|
2412
|
+
async doSync(id) {
|
|
2413
|
+
const existing = this.records.get(id);
|
|
2414
|
+
if (existing?.builtin) return;
|
|
2415
|
+
const source = this.sources.find((s) => s.get(id));
|
|
2416
|
+
const entry = source?.get(id);
|
|
2417
|
+
if (!source || !entry) {
|
|
2418
|
+
if (existing) {
|
|
2419
|
+
existing.hostScope?.dispose();
|
|
2420
|
+
this.records.delete(id);
|
|
2421
|
+
this.changed();
|
|
2422
|
+
}
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const record = existing ?? {
|
|
2426
|
+
rpc: /* @__PURE__ */ new Map(),
|
|
2427
|
+
info: source.toInfo(entry)
|
|
2428
|
+
};
|
|
2429
|
+
this.records.set(id, record);
|
|
2430
|
+
if (record.source !== source) {
|
|
2431
|
+
this.deactivateHost(record);
|
|
2432
|
+
record.source = source;
|
|
2433
|
+
}
|
|
2434
|
+
const info = source.toInfo(entry);
|
|
2435
|
+
if (this.deps.isDisabled(id)) {
|
|
2436
|
+
this.deactivateHost(record);
|
|
2437
|
+
record.info = {
|
|
2438
|
+
...info,
|
|
2439
|
+
status: "disabled",
|
|
2440
|
+
uiBundleUrl: void 0,
|
|
2441
|
+
uiStylesUrl: void 0
|
|
2442
|
+
};
|
|
2443
|
+
this.changed();
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (entry.status !== "active" || !entry.buildId) {
|
|
2447
|
+
record.info = info;
|
|
2448
|
+
this.changed();
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (!entry.hasHost) this.deactivateHost(record);
|
|
2452
|
+
else if (record.activeBuildId !== entry.buildId || !record.hostScope) {
|
|
2453
|
+
this.deactivateHost(record);
|
|
2454
|
+
try {
|
|
2455
|
+
await this.activateHost(record, source.loadHost(id));
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
record.info = {
|
|
2458
|
+
...info,
|
|
2459
|
+
status: "activation-failed",
|
|
2460
|
+
diagnostics: [{
|
|
2461
|
+
source: "activation",
|
|
2462
|
+
message: errorText(error)
|
|
2463
|
+
}]
|
|
2464
|
+
};
|
|
2465
|
+
this.changed();
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
record.activeBuildId = entry.buildId;
|
|
2470
|
+
record.info = info;
|
|
2471
|
+
this.changed();
|
|
2472
|
+
}
|
|
2473
|
+
deactivateHost(record) {
|
|
2474
|
+
record.hostScope?.dispose();
|
|
2475
|
+
record.hostScope = void 0;
|
|
2476
|
+
record.activeBuildId = void 0;
|
|
2477
|
+
record.rpc.clear();
|
|
2478
|
+
}
|
|
2479
|
+
async activateHost(record, extension) {
|
|
2480
|
+
const scope = new Scope();
|
|
2481
|
+
const id = record.info.id;
|
|
2482
|
+
const log = (level) => (...args) => {
|
|
2483
|
+
const message = args.map((a) => typeof a === "string" ? a : safeJson(a)).join(" ");
|
|
2484
|
+
this.reportLog({
|
|
2485
|
+
extensionId: id,
|
|
2486
|
+
level,
|
|
2487
|
+
message,
|
|
2488
|
+
timestamp: Date.now()
|
|
2489
|
+
});
|
|
2490
|
+
console[level === "info" ? "log" : level](`[${id}]`, ...args);
|
|
2491
|
+
};
|
|
2492
|
+
const ctx = {
|
|
2493
|
+
extension: {
|
|
2494
|
+
id,
|
|
2495
|
+
displayName: record.info.displayName,
|
|
2496
|
+
path: record.info.path
|
|
2497
|
+
},
|
|
2498
|
+
workspace: this.deps.workspace,
|
|
2499
|
+
agent: { registerTool: (tool) => scope.add(this.deps.agent.registerTool(id, tool)) },
|
|
2500
|
+
rpc: {
|
|
2501
|
+
handle: (method, handler) => {
|
|
2502
|
+
record.rpc.set(method, handler);
|
|
2503
|
+
return scope.add({ dispose: () => {
|
|
2504
|
+
if (record.rpc.get(method) === handler) record.rpc.delete(method);
|
|
2505
|
+
} });
|
|
2506
|
+
},
|
|
2507
|
+
emit: (event, payload) => this.deps.onEvent(id, event, payload)
|
|
2508
|
+
},
|
|
2509
|
+
secrets: {
|
|
2510
|
+
get: (key) => this.deps.secrets.get(`ext.${id}.${key}`),
|
|
2511
|
+
set: (key, value) => this.deps.secrets.set(`ext.${id}.${key}`, value),
|
|
2512
|
+
delete: (key) => this.deps.secrets.delete(`ext.${id}.${key}`),
|
|
2513
|
+
prompt: async (key, title) => {
|
|
2514
|
+
const value = await this.deps.promptSecret(title);
|
|
2515
|
+
if (value) await this.deps.secrets.set(`ext.${id}.${key}`, value);
|
|
2516
|
+
return value;
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
storage: {
|
|
2520
|
+
get: async (key) => await this.deps.storage.get(id, key),
|
|
2521
|
+
set: (key, value) => this.deps.storage.set(id, key, value)
|
|
2522
|
+
},
|
|
2523
|
+
fs: this.deps.fs,
|
|
2524
|
+
log: {
|
|
2525
|
+
info: log("info"),
|
|
2526
|
+
warn: log("warn"),
|
|
2527
|
+
error: log("error")
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
const returned = await extension.activate(ctx);
|
|
2531
|
+
for (const disposable of returned ?? []) scope.add(disposable);
|
|
2532
|
+
record.hostScope = scope;
|
|
2533
|
+
}
|
|
2534
|
+
changed() {
|
|
2535
|
+
this.deps.onChanged(this.list());
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
var errorText = (error) => error instanceof Error ? error.stack ?? error.message : String(error);
|
|
2539
|
+
var safeJson = (value) => {
|
|
2540
|
+
try {
|
|
2541
|
+
return JSON.stringify(value);
|
|
2542
|
+
} catch {
|
|
2543
|
+
return String(value);
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
//#endregion
|
|
2547
|
+
//#region src/main/services/store/LayeredSettings.ts
|
|
2548
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2549
|
+
/**
|
|
2550
|
+
* Folder settings win: objects merge key by key, keybindings add to the
|
|
2551
|
+
* root ones (registered later, so they take precedence), and any other
|
|
2552
|
+
* value — arrays included — replaces the root's.
|
|
2553
|
+
*/
|
|
2554
|
+
var mergeSettings = (root, folder) => {
|
|
2555
|
+
const merge = (base, over) => {
|
|
2556
|
+
const result = { ...base };
|
|
2557
|
+
for (const [key, value] of Object.entries(over)) {
|
|
2558
|
+
const current = result[key];
|
|
2559
|
+
if (key === "keybindings" && Array.isArray(current)) result[key] = [...current, ...Array.isArray(value) ? value : []];
|
|
2560
|
+
else if (isPlainObject(current) && isPlainObject(value)) result[key] = merge(current, value);
|
|
2561
|
+
else if (value !== void 0) result[key] = value;
|
|
2562
|
+
}
|
|
2563
|
+
return result;
|
|
2564
|
+
};
|
|
2565
|
+
return merge(root, folder);
|
|
2566
|
+
};
|
|
2567
|
+
/** The settings one folder tab sees: `~/.nubase/settings.json` with the folder's `.nubase/settings.json` on top. */
|
|
2568
|
+
var LayeredSettings = class {
|
|
2569
|
+
folder = {};
|
|
2570
|
+
folderFile;
|
|
2571
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2572
|
+
disposers = [];
|
|
2573
|
+
constructor(root, folderSettingsPath) {
|
|
2574
|
+
this.root = root;
|
|
2575
|
+
if (folderSettingsPath) this.folderFile = new JsonFile(folderSettingsPath);
|
|
2576
|
+
}
|
|
2577
|
+
async load() {
|
|
2578
|
+
this.disposers.push(this.root.onChange(() => this.notify()));
|
|
2579
|
+
if (!this.folderFile) return;
|
|
2580
|
+
this.folder = await this.folderFile.read() ?? {};
|
|
2581
|
+
this.disposers.push(this.folderFile.onExternalChange((value) => {
|
|
2582
|
+
this.folder = isPlainObject(value) ? value : {};
|
|
2583
|
+
this.notify();
|
|
2584
|
+
}, { createDir: false }));
|
|
2585
|
+
}
|
|
2586
|
+
get() {
|
|
2587
|
+
return mergeSettings(this.root.get(), this.folder);
|
|
2588
|
+
}
|
|
2589
|
+
getFolderOverrides() {
|
|
2590
|
+
return this.folder;
|
|
2591
|
+
}
|
|
2592
|
+
async update(patch, scope = "root") {
|
|
2593
|
+
if (scope === "root") {
|
|
2594
|
+
await this.root.update(patch);
|
|
2595
|
+
return this.get();
|
|
2596
|
+
}
|
|
2597
|
+
if (!this.folderFile) throw new Error("The ~ tab has no folder settings; use the root scope");
|
|
2598
|
+
this.folder = {
|
|
2599
|
+
...this.folder,
|
|
2600
|
+
...patch
|
|
2601
|
+
};
|
|
2602
|
+
await this.folderFile.write(this.folder);
|
|
2603
|
+
this.notify();
|
|
2604
|
+
return this.get();
|
|
2605
|
+
}
|
|
2606
|
+
onChange(listener) {
|
|
2607
|
+
this.listeners.add(listener);
|
|
2608
|
+
return () => this.listeners.delete(listener);
|
|
2609
|
+
}
|
|
2610
|
+
dispose() {
|
|
2611
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
2612
|
+
this.listeners.clear();
|
|
2613
|
+
}
|
|
2614
|
+
notify() {
|
|
2615
|
+
const value = this.get();
|
|
2616
|
+
for (const listener of this.listeners) listener(value);
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
//#endregion
|
|
2620
|
+
//#region src/main/services/workspaces/Workspace.ts
|
|
2621
|
+
/** Collapses a pre-folder-tabs layout (several tabs) into its active tab. */
|
|
2622
|
+
var migrateLegacyLayout = async (target) => {
|
|
2623
|
+
if (existsSync(target.path) || !existsSync(paths.legacyLayout)) return;
|
|
2624
|
+
const legacy = await new JsonFile(paths.legacyLayout).read();
|
|
2625
|
+
const tab = legacy?.tabs?.find((t) => t.id === legacy.activeTabId) ?? legacy?.tabs?.[0];
|
|
2626
|
+
if (!legacy || !tab) return;
|
|
2627
|
+
await target.write({
|
|
2628
|
+
...legacy,
|
|
2629
|
+
tabs: [tab],
|
|
2630
|
+
activeTabId: tab.id
|
|
2631
|
+
});
|
|
2632
|
+
};
|
|
2633
|
+
/**
|
|
2634
|
+
* One folder tab: its own renderer (view), host server, merged settings,
|
|
2635
|
+
* agent (working in the folder) and extension host. Built-ins and global
|
|
2636
|
+
* extensions come from `~/.nubase`; the folder's `.nubase` overrides them.
|
|
2637
|
+
*/
|
|
2638
|
+
var Workspace = class {
|
|
2639
|
+
view;
|
|
2640
|
+
server = new HostServer();
|
|
2641
|
+
settings;
|
|
2642
|
+
agent;
|
|
2643
|
+
extensions;
|
|
2644
|
+
folderExtensions;
|
|
2645
|
+
layoutFile;
|
|
2646
|
+
fsWatches = /* @__PURE__ */ new Map();
|
|
2647
|
+
watchCounter = 0;
|
|
2648
|
+
disposers = [];
|
|
2649
|
+
constructor(id, folder, shared, tabs) {
|
|
2650
|
+
this.id = id;
|
|
2651
|
+
this.folder = folder;
|
|
2652
|
+
this.shared = shared;
|
|
2653
|
+
this.tabs = tabs;
|
|
2654
|
+
const isHome = this.info.isHome;
|
|
2655
|
+
const local = isHome ? void 0 : folderPaths(folder);
|
|
2656
|
+
const state = workspacePaths(id);
|
|
2657
|
+
const layered = local && local.dir !== paths.home ? local : void 0;
|
|
2658
|
+
this.settings = new LayeredSettings(shared.rootSettings, layered?.settings);
|
|
2659
|
+
this.layoutFile = new JsonFile(state.layout);
|
|
2660
|
+
this.view = new WebContentsView({ webPreferences: {
|
|
2661
|
+
preload: shared.preloadPath,
|
|
2662
|
+
contextIsolation: true,
|
|
2663
|
+
nodeIntegration: false,
|
|
2664
|
+
sandbox: true
|
|
2665
|
+
} });
|
|
2666
|
+
this.view.setBackgroundColor("#0a0a0a");
|
|
2667
|
+
this.view.webContents.setWindowOpenHandler(({ url }) => {
|
|
2668
|
+
shell.openExternal(url);
|
|
2669
|
+
return { action: "deny" };
|
|
2670
|
+
});
|
|
2671
|
+
this.server.connect(createElectronTransport(this.view.webContents));
|
|
2672
|
+
const rootSettings = shared.rootSettings.get();
|
|
2673
|
+
const agentCwd = isHome ? rootSettings.agent.cwd ?? homedir() : folder;
|
|
2674
|
+
this.agent = new AgentHost({
|
|
2675
|
+
cwd: agentCwd,
|
|
2676
|
+
createHarness: (emit) => new PiHarness({
|
|
2677
|
+
agentDir: paths.agentDir,
|
|
2678
|
+
cwd: agentCwd,
|
|
2679
|
+
model: rootSettings.agent.model,
|
|
2680
|
+
skillPaths: [join(shared.resourcesDir, "skills")],
|
|
2681
|
+
appendSystemPrompt: nubaseSystemPrompt({
|
|
2682
|
+
folder,
|
|
2683
|
+
isHome,
|
|
2684
|
+
nubaseHome: paths.home,
|
|
2685
|
+
globalExtensionsDir: paths.extensions,
|
|
2686
|
+
folderNubaseDir: layered?.dir
|
|
2687
|
+
}),
|
|
2688
|
+
dialogs: this.agentDialogs(),
|
|
2689
|
+
emit
|
|
2690
|
+
}),
|
|
2691
|
+
onEvent: (event) => this.server.emit("agent.event", event)
|
|
2692
|
+
});
|
|
2693
|
+
if (layered && existsSync(layered.extensions)) this.folderExtensions = this.createFolderSource(layered.extensions);
|
|
2694
|
+
this.extensions = new ExtensionHost({
|
|
2695
|
+
workspace: this.info,
|
|
2696
|
+
sources: [this.folderExtensions, shared.globalExtensions].filter((s) => Boolean(s)),
|
|
2697
|
+
agent: this.agent,
|
|
2698
|
+
fs: shared.fs,
|
|
2699
|
+
secrets: shared.secrets,
|
|
2700
|
+
storage: shared.storage,
|
|
2701
|
+
safeMode: shared.safeMode,
|
|
2702
|
+
builtins: BUILTIN_EXTENSIONS.map((b) => ({
|
|
2703
|
+
...b,
|
|
2704
|
+
host: b.id === "terminal" ? createTerminalHost({
|
|
2705
|
+
PI_CODING_AGENT_DIR: paths.agentDir,
|
|
2706
|
+
NUBASE_EXTENSIONS_DIR: paths.extensions,
|
|
2707
|
+
NUBASE_FOLDER: folder
|
|
2708
|
+
}) : b.id === "files" ? createFilesHost() : void 0
|
|
2709
|
+
})),
|
|
2710
|
+
isDisabled: (extensionId) => this.settings.get().disabledExtensions.includes(extensionId),
|
|
2711
|
+
promptSecret: (title) => this.server.requestRenderer("dialogs.input", title, { secret: true }),
|
|
2712
|
+
onChanged: (list) => this.server.emit("extensions.changed", list),
|
|
2713
|
+
onEvent: (extensionId, event, payload) => this.server.emit("extensions.event", {
|
|
2714
|
+
extensionId,
|
|
2715
|
+
event,
|
|
2716
|
+
payload
|
|
2717
|
+
})
|
|
2718
|
+
});
|
|
2719
|
+
for (const tool of createNubaseTools({
|
|
2720
|
+
server: this.server,
|
|
2721
|
+
extensions: this.extensions,
|
|
2722
|
+
globalExtensionsDir: paths.extensions,
|
|
2723
|
+
folderExtensionsDir: layered?.extensions,
|
|
2724
|
+
ensureFolderExtensions: async () => {
|
|
2725
|
+
if (!layered) throw new Error("The ~ tab has no folder extensions");
|
|
2726
|
+
await this.ensureFolderExtensions(layered.extensions);
|
|
2727
|
+
return layered.extensions;
|
|
2728
|
+
},
|
|
2729
|
+
templateDir: join(shared.resourcesDir, "templates", "extension"),
|
|
2730
|
+
getWebContents: () => this.view.webContents.isDestroyed() ? void 0 : this.view.webContents
|
|
2731
|
+
})) this.agent.registerTool("nubase", tool);
|
|
2732
|
+
this.registerHandlers(layered?.extensions);
|
|
2733
|
+
}
|
|
2734
|
+
get info() {
|
|
2735
|
+
return this.tabs.info(this.id);
|
|
2736
|
+
}
|
|
2737
|
+
/** The folder's own extensions, if it has any; asset URLs use the workspace id as their key. */
|
|
2738
|
+
get folderSource() {
|
|
2739
|
+
return this.folderExtensions;
|
|
2740
|
+
}
|
|
2741
|
+
async start() {
|
|
2742
|
+
if (this.info.isHome) await migrateLegacyLayout(this.layoutFile);
|
|
2743
|
+
await this.settings.load();
|
|
2744
|
+
this.disposers.push(this.settings.onChange((value) => {
|
|
2745
|
+
this.server.emit("settings.changed", value);
|
|
2746
|
+
this.extensions.refresh();
|
|
2747
|
+
}), this.layoutFile.onExternalChange((layout) => this.server.emit("layout.changedOnDisk", layout)));
|
|
2748
|
+
const { rendererUrl } = this.shared;
|
|
2749
|
+
if ("url" in rendererUrl) this.view.webContents.loadURL(rendererUrl.url);
|
|
2750
|
+
else this.view.webContents.loadFile(rendererUrl.file);
|
|
2751
|
+
this.agent.start();
|
|
2752
|
+
this.extensions.start().catch((error) => console.error(`Extensions failed to start for ${this.folder}`, error));
|
|
2753
|
+
}
|
|
2754
|
+
dispose() {
|
|
2755
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
2756
|
+
for (const watch of this.fsWatches.values()) watch.dispose();
|
|
2757
|
+
this.fsWatches.clear();
|
|
2758
|
+
this.extensions.dispose();
|
|
2759
|
+
this.agent.dispose();
|
|
2760
|
+
this.folderExtensions?.dispose();
|
|
2761
|
+
this.settings.dispose();
|
|
2762
|
+
if (!this.view.webContents.isDestroyed()) this.view.webContents.close();
|
|
2763
|
+
}
|
|
2764
|
+
createFolderSource(dir) {
|
|
2765
|
+
return new ExtensionSource({
|
|
2766
|
+
key: this.id,
|
|
2767
|
+
kind: "folder",
|
|
2768
|
+
dir,
|
|
2769
|
+
builder: this.shared.builder,
|
|
2770
|
+
repo: new ExtensionsRepo(dir, workspacePaths(this.id).extensionsGitDir)
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
async ensureFolderExtensions(dir) {
|
|
2774
|
+
if (this.folderExtensions) return;
|
|
2775
|
+
await prepareExtensionsWorkspace(dir, this.shared.resourcesDir, { inProject: true });
|
|
2776
|
+
this.folderExtensions = this.createFolderSource(dir);
|
|
2777
|
+
await this.extensions.addSource(this.folderExtensions);
|
|
2778
|
+
}
|
|
2779
|
+
agentDialogs() {
|
|
2780
|
+
const server = this.server;
|
|
2781
|
+
return {
|
|
2782
|
+
confirm: (title, message) => server.requestRenderer("dialogs.confirm", title, message),
|
|
2783
|
+
input: (title, options) => {
|
|
2784
|
+
const { signal, ...inputOptions } = options ?? {};
|
|
2785
|
+
if (signal?.aborted) return Promise.resolve(void 0);
|
|
2786
|
+
const dialogId = randomUUID();
|
|
2787
|
+
const cancel = () => {
|
|
2788
|
+
server.requestRenderer("dialogs.cancelInput", dialogId).catch(() => {});
|
|
2789
|
+
};
|
|
2790
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
2791
|
+
return server.requestRenderer("dialogs.input", title, {
|
|
2792
|
+
...inputOptions,
|
|
2793
|
+
dialogId
|
|
2794
|
+
}).finally(() => signal?.removeEventListener("abort", cancel));
|
|
2795
|
+
},
|
|
2796
|
+
select: (title, options) => server.requestRenderer("dialogs.select", title, options),
|
|
2797
|
+
notify: (message, level) => void server.requestRenderer("notify", message, level).catch(() => {}),
|
|
2798
|
+
openExternal: (url) => {
|
|
2799
|
+
if (/^https?:\/\//.test(url)) shell.openExternal(url);
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
registerHandlers(folderExtensionsDir) {
|
|
2804
|
+
const { fs, storage, safeMode } = this.shared;
|
|
2805
|
+
const { server, settings, agent, extensions } = this;
|
|
2806
|
+
server.handle({
|
|
2807
|
+
"app.info": () => ({
|
|
2808
|
+
version: this.shared.appVersion(),
|
|
2809
|
+
platform: process.platform,
|
|
2810
|
+
homeDir: fs.homeDir,
|
|
2811
|
+
nubaseHome: paths.home,
|
|
2812
|
+
extensionsDir: paths.extensions,
|
|
2813
|
+
folderExtensionsDir,
|
|
2814
|
+
safeMode,
|
|
2815
|
+
workspace: this.info
|
|
2816
|
+
}),
|
|
2817
|
+
"app.openExternal": (url) => {
|
|
2818
|
+
if (/^https?:\/\//.test(url)) shell.openExternal(url);
|
|
2819
|
+
},
|
|
2820
|
+
"app.reportLog": (entry) => extensions.reportLog(entry),
|
|
2821
|
+
"fs.readText": (path) => fs.readText(path),
|
|
2822
|
+
"fs.writeText": (path, content) => fs.writeText(path, content),
|
|
2823
|
+
"fs.stat": (path) => fs.stat(path),
|
|
2824
|
+
"fs.readDirectory": (path) => fs.readDirectory(path),
|
|
2825
|
+
"fs.mkdir": (path) => fs.mkdir(path),
|
|
2826
|
+
"fs.delete": (path, recursive) => fs.delete(path, { recursive }),
|
|
2827
|
+
"fs.rename": (from, to) => fs.rename(from, to),
|
|
2828
|
+
"fs.watch": (path, recursive) => {
|
|
2829
|
+
const watchId = `w${this.watchCounter++}`;
|
|
2830
|
+
this.fsWatches.set(watchId, fs.watch(path, (change) => server.emit("fs.change", {
|
|
2831
|
+
watchId,
|
|
2832
|
+
change
|
|
2833
|
+
}), { recursive }));
|
|
2834
|
+
return watchId;
|
|
2835
|
+
},
|
|
2836
|
+
"fs.unwatch": (watchId) => {
|
|
2837
|
+
this.fsWatches.get(watchId)?.dispose();
|
|
2838
|
+
this.fsWatches.delete(watchId);
|
|
2839
|
+
},
|
|
2840
|
+
"fs.thumbnail": async (path, size) => {
|
|
2841
|
+
if (process.platform !== "darwin" && process.platform !== "win32") return;
|
|
2842
|
+
try {
|
|
2843
|
+
const image = await nativeImage.createThumbnailFromPath(path, {
|
|
2844
|
+
width: size,
|
|
2845
|
+
height: size
|
|
2846
|
+
});
|
|
2847
|
+
return image.isEmpty() ? void 0 : image.toDataURL();
|
|
2848
|
+
} catch {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
},
|
|
2852
|
+
"shell.openPath": async (path) => {
|
|
2853
|
+
const error = await shell.openPath(path);
|
|
2854
|
+
if (error) throw new Error(error);
|
|
2855
|
+
},
|
|
2856
|
+
"shell.showItemInFolder": (path) => shell.showItemInFolder(path),
|
|
2857
|
+
"shell.quickLook": (path) => {
|
|
2858
|
+
if (process.platform !== "darwin") return false;
|
|
2859
|
+
const window = BaseWindow.getAllWindows().find((w) => w.contentView.children.includes(this.view));
|
|
2860
|
+
if (!window) return false;
|
|
2861
|
+
window.previewFile(path);
|
|
2862
|
+
return true;
|
|
2863
|
+
},
|
|
2864
|
+
"settings.get": () => settings.get(),
|
|
2865
|
+
"settings.update": (patch, scope) => settings.update(patch, scope),
|
|
2866
|
+
"settings.getFolderOverrides": () => settings.getFolderOverrides(),
|
|
2867
|
+
"layout.load": () => this.layoutFile.read(),
|
|
2868
|
+
"layout.save": (layout) => this.layoutFile.write(layout),
|
|
2869
|
+
"storage.get": (scope, key) => storage.get(scope, key),
|
|
2870
|
+
"storage.set": (scope, key, value) => storage.set(scope, key, value),
|
|
2871
|
+
"workspaces.list": () => this.tabs.list(),
|
|
2872
|
+
"workspaces.open": (folder) => this.tabs.open(folder),
|
|
2873
|
+
"workspaces.close": (id) => this.tabs.close(id),
|
|
2874
|
+
"workspaces.activate": (id) => this.tabs.activate(id),
|
|
2875
|
+
"agent.getState": () => agent.getState(),
|
|
2876
|
+
"agent.getMessages": () => agent.getMessages(),
|
|
2877
|
+
"agent.prompt": (text, images) => agent.prompt(text, images),
|
|
2878
|
+
"agent.steer": (text, images) => agent.steer(text, images),
|
|
2879
|
+
"agent.abort": () => agent.abort(),
|
|
2880
|
+
"agent.newSession": () => agent.newSession(),
|
|
2881
|
+
"agent.listSessions": () => agent.listSessions(),
|
|
2882
|
+
"agent.switchSession": (path) => agent.switchSession(path),
|
|
2883
|
+
"agent.listModels": () => agent.listModels(),
|
|
2884
|
+
"agent.setModel": async (provider, id) => {
|
|
2885
|
+
await agent.setModel(provider, id);
|
|
2886
|
+
await settings.update({ agent: {
|
|
2887
|
+
...this.shared.rootSettings.get().agent,
|
|
2888
|
+
model: {
|
|
2889
|
+
provider,
|
|
2890
|
+
id
|
|
2891
|
+
}
|
|
2892
|
+
} });
|
|
2893
|
+
},
|
|
2894
|
+
"agent.listProviders": () => agent.listProviders(),
|
|
2895
|
+
"agent.login": (providerId, method) => agent.login(providerId, method),
|
|
2896
|
+
"agent.logout": (providerId) => agent.logout(providerId),
|
|
2897
|
+
"extensions.list": () => extensions.list(),
|
|
2898
|
+
"extensions.rebuild": (id) => extensions.rebuild(id),
|
|
2899
|
+
"extensions.setEnabled": async (id, enabled) => {
|
|
2900
|
+
const disabled = new Set(settings.get().disabledExtensions);
|
|
2901
|
+
if (enabled) disabled.delete(id);
|
|
2902
|
+
else disabled.add(id);
|
|
2903
|
+
const scope = settings.getFolderOverrides().disabledExtensions !== void 0 ? "folder" : "root";
|
|
2904
|
+
await settings.update({ disabledExtensions: [...disabled] }, scope);
|
|
2905
|
+
await extensions.setEnabled(id);
|
|
2906
|
+
},
|
|
2907
|
+
"extensions.revert": (id) => extensions.revert(id),
|
|
2908
|
+
"extensions.reveal": (id) => {
|
|
2909
|
+
shell.showItemInFolder(join(extensions.pathOf(id), "package.json"));
|
|
2910
|
+
},
|
|
2911
|
+
"extensions.call": (extensionId, method, args) => extensions.call(extensionId, method, args)
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
//#endregion
|
|
2916
|
+
//#region src/main/services/workspaces/WorkspaceManager.ts
|
|
2917
|
+
var isDirectory = (path) => {
|
|
2918
|
+
try {
|
|
2919
|
+
return statSync(path).isDirectory();
|
|
2920
|
+
} catch {
|
|
2921
|
+
return false;
|
|
2922
|
+
}
|
|
2923
|
+
};
|
|
2924
|
+
/**
|
|
2925
|
+
* The tab bar: which folders are open, which one is showing, and the
|
|
2926
|
+
* folder tabs (each a Workspace with its own renderer view) behind them.
|
|
2927
|
+
* `~` is always the first tab and cannot be closed.
|
|
2928
|
+
*/
|
|
2929
|
+
var WorkspaceManager = class {
|
|
2930
|
+
folders = [];
|
|
2931
|
+
activeId = HOME_WORKSPACE_ID;
|
|
2932
|
+
workspaces = /* @__PURE__ */ new Map();
|
|
2933
|
+
starting = /* @__PURE__ */ new Map();
|
|
2934
|
+
window;
|
|
2935
|
+
file = new JsonFile(paths.workspaces);
|
|
2936
|
+
constructor(shared) {
|
|
2937
|
+
this.shared = shared;
|
|
2938
|
+
}
|
|
2939
|
+
async restore() {
|
|
2940
|
+
const stored = await this.file.read();
|
|
2941
|
+
const home = homedir();
|
|
2942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2943
|
+
this.folders = (stored?.folders ?? []).map((folder) => resolve(folder)).filter((folder) => {
|
|
2944
|
+
if (folder === home || seen.has(folder) || !isDirectory(folder)) return false;
|
|
2945
|
+
seen.add(folder);
|
|
2946
|
+
return true;
|
|
2947
|
+
});
|
|
2948
|
+
const activeId = stored?.activeId;
|
|
2949
|
+
this.activeId = activeId && this.folderOf(activeId) ? activeId : HOME_WORKSPACE_ID;
|
|
2950
|
+
}
|
|
2951
|
+
/** Shows the folder tabs in a (new) window, starting with the active one. */
|
|
2952
|
+
async attach(window) {
|
|
2953
|
+
this.window = window;
|
|
2954
|
+
window.on("resize", () => this.layoutViews());
|
|
2955
|
+
window.on("closed", () => {
|
|
2956
|
+
if (this.window === window) this.window = void 0;
|
|
2957
|
+
for (const workspace of this.workspaces.values()) workspace.dispose();
|
|
2958
|
+
this.workspaces.clear();
|
|
2959
|
+
});
|
|
2960
|
+
await this.activate(this.activeId);
|
|
2961
|
+
}
|
|
2962
|
+
list() {
|
|
2963
|
+
return {
|
|
2964
|
+
workspaces: this.ids().map((id) => this.info(id)),
|
|
2965
|
+
activeId: this.activeId
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
info(id) {
|
|
2969
|
+
const folder = this.folderOf(id);
|
|
2970
|
+
if (!folder) throw new Error(`No open folder "${id}"`);
|
|
2971
|
+
if (id === "home") return {
|
|
2972
|
+
id,
|
|
2973
|
+
folder,
|
|
2974
|
+
title: "~",
|
|
2975
|
+
isHome: true
|
|
2976
|
+
};
|
|
2977
|
+
const name = basename(folder) || folder;
|
|
2978
|
+
return {
|
|
2979
|
+
id,
|
|
2980
|
+
folder,
|
|
2981
|
+
title: this.folders.some((other) => other !== folder && basename(other) === name) ? `${basename(dirname(folder))}/${name}` : name,
|
|
2982
|
+
isHome: false
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
async open(folder) {
|
|
2986
|
+
let target = folder;
|
|
2987
|
+
if (!target) {
|
|
2988
|
+
const options = {
|
|
2989
|
+
title: "Open Folder",
|
|
2990
|
+
properties: ["openDirectory", "createDirectory"]
|
|
2991
|
+
};
|
|
2992
|
+
const result = this.window ? await dialog.showOpenDialog(this.window, options) : await dialog.showOpenDialog(options);
|
|
2993
|
+
target = result.canceled ? void 0 : result.filePaths[0];
|
|
2994
|
+
if (!target) return void 0;
|
|
2995
|
+
}
|
|
2996
|
+
target = resolve(target);
|
|
2997
|
+
if (!isDirectory(target)) throw new Error(`Not a folder: ${target}`);
|
|
2998
|
+
const id = workspaceIdOf(target);
|
|
2999
|
+
if (id !== "home" && !this.folders.includes(target)) this.folders.push(target);
|
|
3000
|
+
await this.activate(id);
|
|
3001
|
+
return this.info(id);
|
|
3002
|
+
}
|
|
3003
|
+
async close(id) {
|
|
3004
|
+
if (id === "home") throw new Error("The ~ tab cannot be closed");
|
|
3005
|
+
const ids = this.ids();
|
|
3006
|
+
const index = ids.indexOf(id);
|
|
3007
|
+
if (index < 0) return;
|
|
3008
|
+
const folder = this.folderOf(id);
|
|
3009
|
+
this.folders = this.folders.filter((f) => f !== folder);
|
|
3010
|
+
const workspace = this.workspaces.get(id);
|
|
3011
|
+
this.workspaces.delete(id);
|
|
3012
|
+
if (workspace) {
|
|
3013
|
+
this.window?.contentView.removeChildView(workspace.view);
|
|
3014
|
+
workspace.dispose();
|
|
3015
|
+
}
|
|
3016
|
+
if (this.activeId === id) await this.activate(ids[index - 1] ?? "home");
|
|
3017
|
+
else {
|
|
3018
|
+
await this.persist();
|
|
3019
|
+
this.broadcast();
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
async activate(id) {
|
|
3023
|
+
if (!this.folderOf(id)) throw new Error(`No open folder "${id}"`);
|
|
3024
|
+
this.activeId = id;
|
|
3025
|
+
await this.persist();
|
|
3026
|
+
this.broadcast();
|
|
3027
|
+
const window = this.window;
|
|
3028
|
+
if (!window) return;
|
|
3029
|
+
const workspace = await this.ensure(id);
|
|
3030
|
+
if (this.activeId !== id || window.isDestroyed()) return;
|
|
3031
|
+
for (const other of this.workspaces.values()) other.view.setVisible(other === workspace);
|
|
3032
|
+
this.layoutViews();
|
|
3033
|
+
workspace.view.webContents.focus();
|
|
3034
|
+
}
|
|
3035
|
+
/** Activates the tab `offset` places away from the current one, wrapping around. */
|
|
3036
|
+
async activateRelative(offset) {
|
|
3037
|
+
const ids = this.ids();
|
|
3038
|
+
const next = ids[(ids.indexOf(this.activeId) + offset + ids.length) % ids.length];
|
|
3039
|
+
if (next) await this.activate(next);
|
|
3040
|
+
}
|
|
3041
|
+
/** Resolves `nubase-ext://<key>/<id>/<file>`: `global`, or a workspace id for that folder's extensions. */
|
|
3042
|
+
resolveAsset(url) {
|
|
3043
|
+
const parsed = new URL(url);
|
|
3044
|
+
if (parsed.protocol !== `nubase-ext:`) return void 0;
|
|
3045
|
+
return (parsed.hostname === this.shared.globalExtensions.key ? this.shared.globalExtensions : this.workspaces.get(parsed.hostname)?.folderSource)?.resolveAsset(parsed.pathname);
|
|
3046
|
+
}
|
|
3047
|
+
dispose() {
|
|
3048
|
+
for (const workspace of this.workspaces.values()) workspace.dispose();
|
|
3049
|
+
this.workspaces.clear();
|
|
3050
|
+
}
|
|
3051
|
+
ids() {
|
|
3052
|
+
return [HOME_WORKSPACE_ID, ...this.folders.map(workspaceIdOf)];
|
|
3053
|
+
}
|
|
3054
|
+
folderOf(id) {
|
|
3055
|
+
if (id === "home") return homedir();
|
|
3056
|
+
return this.folders.find((folder) => workspaceIdOf(folder) === id);
|
|
3057
|
+
}
|
|
3058
|
+
ensure(id) {
|
|
3059
|
+
const existing = this.workspaces.get(id);
|
|
3060
|
+
if (existing) return Promise.resolve(existing);
|
|
3061
|
+
let pending = this.starting.get(id);
|
|
3062
|
+
if (!pending) {
|
|
3063
|
+
pending = (async () => {
|
|
3064
|
+
const folder = this.folderOf(id);
|
|
3065
|
+
if (!folder) throw new Error(`No open folder "${id}"`);
|
|
3066
|
+
const workspace = new Workspace(id, folder, this.shared, {
|
|
3067
|
+
info: (wid) => this.info(wid),
|
|
3068
|
+
list: () => this.list(),
|
|
3069
|
+
open: (f) => this.open(f),
|
|
3070
|
+
close: (wid) => this.close(wid),
|
|
3071
|
+
activate: (wid) => this.activate(wid)
|
|
3072
|
+
});
|
|
3073
|
+
workspace.view.setVisible(false);
|
|
3074
|
+
this.window?.contentView.addChildView(workspace.view);
|
|
3075
|
+
this.workspaces.set(id, workspace);
|
|
3076
|
+
this.layoutViews();
|
|
3077
|
+
await workspace.start();
|
|
3078
|
+
return workspace;
|
|
3079
|
+
})().finally(() => this.starting.delete(id));
|
|
3080
|
+
this.starting.set(id, pending);
|
|
3081
|
+
}
|
|
3082
|
+
return pending;
|
|
3083
|
+
}
|
|
3084
|
+
layoutViews() {
|
|
3085
|
+
if (!this.window || this.window.isDestroyed()) return;
|
|
3086
|
+
const { width, height } = this.window.getContentBounds();
|
|
3087
|
+
for (const workspace of this.workspaces.values()) workspace.view.setBounds({
|
|
3088
|
+
x: 0,
|
|
3089
|
+
y: 0,
|
|
3090
|
+
width,
|
|
3091
|
+
height
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
broadcast() {
|
|
3095
|
+
const list = this.list();
|
|
3096
|
+
for (const workspace of this.workspaces.values()) workspace.server.emit("workspaces.changed", list);
|
|
3097
|
+
}
|
|
3098
|
+
persist() {
|
|
3099
|
+
return this.file.write({
|
|
3100
|
+
folders: this.folders,
|
|
3101
|
+
activeId: this.activeId
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
//#endregion
|
|
3106
|
+
//#region src/main/index.ts
|
|
3107
|
+
var safeMode = process.argv.includes("--safe-mode");
|
|
3108
|
+
protocol.registerSchemesAsPrivileged([{
|
|
3109
|
+
scheme: EXTENSION_PROTOCOL,
|
|
3110
|
+
privileges: {
|
|
3111
|
+
standard: true,
|
|
3112
|
+
secure: true,
|
|
3113
|
+
supportFetchAPI: true,
|
|
3114
|
+
corsEnabled: true,
|
|
3115
|
+
stream: true
|
|
3116
|
+
}
|
|
3117
|
+
}, {
|
|
3118
|
+
scheme: FILE_PROTOCOL,
|
|
3119
|
+
privileges: {
|
|
3120
|
+
standard: true,
|
|
3121
|
+
secure: true,
|
|
3122
|
+
supportFetchAPI: true,
|
|
3123
|
+
corsEnabled: true,
|
|
3124
|
+
stream: true
|
|
3125
|
+
}
|
|
3126
|
+
}]);
|
|
3127
|
+
var MIME_TYPES = {
|
|
3128
|
+
js: "text/javascript",
|
|
3129
|
+
css: "text/css",
|
|
3130
|
+
json: "application/json",
|
|
3131
|
+
map: "application/json",
|
|
3132
|
+
png: "image/png",
|
|
3133
|
+
svg: "image/svg+xml"
|
|
3134
|
+
};
|
|
3135
|
+
/** The window is only a frame: each folder tab is a view inside it with its own renderer. */
|
|
3136
|
+
var createWindow = () => {
|
|
3137
|
+
return new BrowserWindow({
|
|
3138
|
+
width: 1440,
|
|
3139
|
+
height: 900,
|
|
3140
|
+
minWidth: 720,
|
|
3141
|
+
minHeight: 480,
|
|
3142
|
+
show: false,
|
|
3143
|
+
backgroundColor: "#0a0a0a",
|
|
3144
|
+
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
|
|
3145
|
+
trafficLightPosition: {
|
|
3146
|
+
x: 14,
|
|
3147
|
+
y: 13
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
};
|
|
3151
|
+
var main = async () => {
|
|
3152
|
+
await app.whenReady();
|
|
3153
|
+
const resourcesDir = join(app.getAppPath(), "resources");
|
|
3154
|
+
const rootSettings = new SettingsService(paths.settings);
|
|
3155
|
+
await rootSettings.load();
|
|
3156
|
+
await prepareExtensionsWorkspace(paths.extensions, resourcesDir);
|
|
3157
|
+
const builder = new ExtensionBuilder();
|
|
3158
|
+
const workspaces = new WorkspaceManager({
|
|
3159
|
+
appVersion: () => app.getVersion(),
|
|
3160
|
+
resourcesDir,
|
|
3161
|
+
preloadPath: join(import.meta.dirname, "../preload/index.cjs"),
|
|
3162
|
+
rendererUrl: process.env.ELECTRON_RENDERER_URL ? { url: process.env.ELECTRON_RENDERER_URL } : { file: join(import.meta.dirname, "../renderer/index.html") },
|
|
3163
|
+
safeMode,
|
|
3164
|
+
fs: new LocalFileSystem(),
|
|
3165
|
+
storage: new StorageService(paths.storage),
|
|
3166
|
+
secrets: new SecretStore(paths.secrets),
|
|
3167
|
+
rootSettings,
|
|
3168
|
+
builder,
|
|
3169
|
+
globalExtensions: new ExtensionSource({
|
|
3170
|
+
key: "global",
|
|
3171
|
+
kind: "global",
|
|
3172
|
+
dir: paths.extensions,
|
|
3173
|
+
builder,
|
|
3174
|
+
repo: new ExtensionsRepo(paths.extensions)
|
|
3175
|
+
})
|
|
3176
|
+
});
|
|
3177
|
+
await workspaces.restore();
|
|
3178
|
+
installApplicationMenu({ openFolder: () => void workspaces.open().catch(console.error) });
|
|
3179
|
+
protocol.handle(EXTENSION_PROTOCOL, async (request) => {
|
|
3180
|
+
const file = workspaces.resolveAsset(request.url);
|
|
3181
|
+
if (!file) return new Response("Not found", { status: 404 });
|
|
3182
|
+
const response = await net.fetch(pathToFileURL(file).toString());
|
|
3183
|
+
const extension = file.split(".").pop() ?? "";
|
|
3184
|
+
return new Response(response.body, {
|
|
3185
|
+
status: response.status,
|
|
3186
|
+
headers: {
|
|
3187
|
+
"Content-Type": MIME_TYPES[extension] ?? "application/octet-stream",
|
|
3188
|
+
"Access-Control-Allow-Origin": "*",
|
|
3189
|
+
"Cache-Control": "no-store"
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
});
|
|
3193
|
+
protocol.handle(FILE_PROTOCOL, handleFileRequest);
|
|
3194
|
+
let mainWindow;
|
|
3195
|
+
const openWindow = async () => {
|
|
3196
|
+
const window = createWindow();
|
|
3197
|
+
mainWindow = window;
|
|
3198
|
+
window.on("closed", () => {
|
|
3199
|
+
if (mainWindow === window) mainWindow = void 0;
|
|
3200
|
+
});
|
|
3201
|
+
await workspaces.attach(window);
|
|
3202
|
+
window.show();
|
|
3203
|
+
};
|
|
3204
|
+
await openWindow();
|
|
3205
|
+
app.on("before-quit", () => workspaces.dispose());
|
|
3206
|
+
app.on("activate", () => {
|
|
3207
|
+
if (!mainWindow) openWindow();
|
|
3208
|
+
});
|
|
3209
|
+
};
|
|
3210
|
+
app.on("window-all-closed", () => {
|
|
3211
|
+
if (process.platform !== "darwin") app.quit();
|
|
3212
|
+
});
|
|
3213
|
+
main().catch((error) => {
|
|
3214
|
+
console.error("Nubase failed to start", error);
|
|
3215
|
+
app.quit();
|
|
3216
|
+
});
|
|
3217
|
+
//#endregion
|
|
3218
|
+
export {};
|