@nubase/terminal 0.0.0-stage → 0.1.1

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.
Files changed (119) hide show
  1. package/bin/nubase.js +11 -0
  2. package/out/main/index.js +3240 -0
  3. package/out/preload/index.cjs +23 -0
  4. package/out/renderer/assets/BinaryPreviews-Dvmgctgl.js +262 -0
  5. package/out/renderer/assets/CodeEditor-BKx61p9c.js +2034 -0
  6. package/out/renderer/assets/DocumentPreviews-BA1gL-ZJ.js +66 -0
  7. package/out/renderer/assets/DocxPreview-CiY-SNbh.js +6955 -0
  8. package/out/renderer/assets/ImagePreview-DpnKfbxj.js +120 -0
  9. package/out/renderer/assets/PdfPreview-6oeuYaqY.js +24399 -0
  10. package/out/renderer/assets/PdfPreview-BT-DZbCt.css +6483 -0
  11. package/out/renderer/assets/SpreadsheetPreviews-B4Qby88A.js +23987 -0
  12. package/out/renderer/assets/abap-Cel38df4.js +1317 -0
  13. package/out/renderer/assets/apex-DzOmV_zF.js +355 -0
  14. package/out/renderer/assets/azcli-Xvapx-7p.js +56 -0
  15. package/out/renderer/assets/bat-BGMSIlWY.js +126 -0
  16. package/out/renderer/assets/bicep-CcpV5qxk.js +228 -0
  17. package/out/renderer/assets/cameligo-BfMDXVEx.js +233 -0
  18. package/out/renderer/assets/clojure-DFgIAJGu.js +801 -0
  19. package/out/renderer/assets/codicon-DF1abBS2.ttf +0 -0
  20. package/out/renderer/assets/coffee-Db4q5aZ8.js +308 -0
  21. package/out/renderer/assets/cpp-CEvPJv6f.js +450 -0
  22. package/out/renderer/assets/csharp-CVoBr97X.js +405 -0
  23. package/out/renderer/assets/csp-Cgzd5gxo.js +49 -0
  24. package/out/renderer/assets/css-D3M-RRVi.js +287 -0
  25. package/out/renderer/assets/css.worker-AXSlELAq.js +50031 -0
  26. package/out/renderer/assets/cssMode-CrMQfSUN.js +101 -0
  27. package/out/renderer/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
  28. package/out/renderer/assets/cypher-DJJgZ_3Q.js +326 -0
  29. package/out/renderer/assets/dart-Bkg7I-Qe.js +341 -0
  30. package/out/renderer/assets/dockerfile-B1r36HyK.js +194 -0
  31. package/out/renderer/assets/ecl-C1Ji6tZG.js +531 -0
  32. package/out/renderer/assets/editor-CXfYqUZ6.css +4816 -0
  33. package/out/renderer/assets/editor.api-zF37fz_e.js +127071 -0
  34. package/out/renderer/assets/elixir-D_-n8lGJ.js +526 -0
  35. package/out/renderer/assets/fileTypes-nBt-Y6NI.js +56306 -0
  36. package/out/renderer/assets/flow9-CCdkJPNN.js +179 -0
  37. package/out/renderer/assets/freemarker2-CBlvIP9I.js +733 -0
  38. package/out/renderer/assets/fsharp-D5KoOpm3.js +248 -0
  39. package/out/renderer/assets/go-B7BFdMAL.js +264 -0
  40. package/out/renderer/assets/graphql-BqIezLde.js +177 -0
  41. package/out/renderer/assets/handlebars-CG4DAk5_.js +414 -0
  42. package/out/renderer/assets/hcl-C1GQCKC0.js +222 -0
  43. package/out/renderer/assets/html-C-_R4s2w.js +335 -0
  44. package/out/renderer/assets/html.worker-vQ8gO4kF.js +30584 -0
  45. package/out/renderer/assets/htmlMode-mGTQJ-lr.js +125 -0
  46. package/out/renderer/assets/index-CQHlaLTi.js +46080 -0
  47. package/out/renderer/assets/index-DBYJcRdn.css +3124 -0
  48. package/out/renderer/assets/ini-DDFxEGYN.js +97 -0
  49. package/out/renderer/assets/java-BqvgSBNj.js +272 -0
  50. package/out/renderer/assets/javascript-BuIet4p-.js +69 -0
  51. package/out/renderer/assets/json.worker-DBhrCMTN.js +23259 -0
  52. package/out/renderer/assets/jsonMode-OAArikPQ.js +769 -0
  53. package/out/renderer/assets/julia-D30HQT3I.js +688 -0
  54. package/out/renderer/assets/kotlin-Bql4-FwM.js +303 -0
  55. package/out/renderer/assets/less-D_rhcFqZ.js +245 -0
  56. package/out/renderer/assets/lexon-CjhVPeai.js +188 -0
  57. package/out/renderer/assets/liquid-DJNDWhYP.js +277 -0
  58. package/out/renderer/assets/lspLanguageFeatures-BIH0_3rO.css +24 -0
  59. package/out/renderer/assets/lspLanguageFeatures-Bvh4jxNk.js +2583 -0
  60. package/out/renderer/assets/lua-HQuy0vuU.js +203 -0
  61. package/out/renderer/assets/m3-CIJ7obE2.js +287 -0
  62. package/out/renderer/assets/markdown-I0GroHKS.js +265 -0
  63. package/out/renderer/assets/mdx-B4We-zdi.js +330 -0
  64. package/out/renderer/assets/mips-DY_Z1uin.js +190 -0
  65. package/out/renderer/assets/msdax-YkUW9rXl.js +416 -0
  66. package/out/renderer/assets/mysql-CIm9uV8f.js +921 -0
  67. package/out/renderer/assets/objective-c-JkhNEfNv.js +233 -0
  68. package/out/renderer/assets/pascal-Dvq-PxOC.js +289 -0
  69. package/out/renderer/assets/pascaligo-A9tZTvdV.js +204 -0
  70. package/out/renderer/assets/pdf_viewer-8SNFor7s.js +7639 -0
  71. package/out/renderer/assets/perl-C2Ujz2HB.js +720 -0
  72. package/out/renderer/assets/pgsql-BHB-cYm3.js +879 -0
  73. package/out/renderer/assets/php-DkNik5Vk.js +548 -0
  74. package/out/renderer/assets/pla-DQ-RrdDE.js +149 -0
  75. package/out/renderer/assets/postiats-f1cpw-r_.js +702 -0
  76. package/out/renderer/assets/powerquery-CwYp-rYn.js +940 -0
  77. package/out/renderer/assets/powershell-DNVb682q.js +252 -0
  78. package/out/renderer/assets/protobuf-C73bXEb8.js +663 -0
  79. package/out/renderer/assets/pug-BxvM69yY.js +419 -0
  80. package/out/renderer/assets/python-Hlw9kU7X.js +389 -0
  81. package/out/renderer/assets/qsharp-BCs2AvEL.js +335 -0
  82. package/out/renderer/assets/r-B7wnzr1V.js +282 -0
  83. package/out/renderer/assets/razor-BswxUzye.js +615 -0
  84. package/out/renderer/assets/redis-BQXcxMfX.js +324 -0
  85. package/out/renderer/assets/redshift-2dZdIUPH.js +838 -0
  86. package/out/renderer/assets/restructuredtext-CVT2zSWP.js +290 -0
  87. package/out/renderer/assets/ruby-DjmMI5Dp.js +612 -0
  88. package/out/renderer/assets/rust-BhSCZj0F.js +382 -0
  89. package/out/renderer/assets/sb-DaUIZery.js +146 -0
  90. package/out/renderer/assets/scala-CYIN7D7O.js +589 -0
  91. package/out/renderer/assets/scheme-DanUmhC2.js +173 -0
  92. package/out/renderer/assets/scss-BVILLTDt.js +420 -0
  93. package/out/renderer/assets/shell-Bpairu2a.js +293 -0
  94. package/out/renderer/assets/solidity-OLm4MSRm.js +1395 -0
  95. package/out/renderer/assets/sophia-DJz2Vk1M.js +234 -0
  96. package/out/renderer/assets/sparql-Bk5uRAQt.js +237 -0
  97. package/out/renderer/assets/sql-DQBN9zz5.js +858 -0
  98. package/out/renderer/assets/st-50mV4H0S.js +657 -0
  99. package/out/renderer/assets/swift-Yc0Q28XL.js +376 -0
  100. package/out/renderer/assets/systemverilog-Du7vdcmu.js +616 -0
  101. package/out/renderer/assets/tcl-DG7u3TBj.js +312 -0
  102. package/out/renderer/assets/toggleHighContrast-CKRp-vZ7.css +3691 -0
  103. package/out/renderer/assets/toggleHighContrast-NyS1pk1Z.js +51247 -0
  104. package/out/renderer/assets/twig-C49MsNA9.js +438 -0
  105. package/out/renderer/assets/typescript-F2yeVXIn.js +382 -0
  106. package/out/renderer/assets/typespec-DA1dz9gQ.js +245 -0
  107. package/out/renderer/assets/useAsync-Bi56CNnI.js +30 -0
  108. package/out/renderer/assets/vb-BDNIgfQk.js +447 -0
  109. package/out/renderer/assets/wgsl-j-0nJ_62.js +457 -0
  110. package/out/renderer/assets/xml-BfG5_Ah8.js +131 -0
  111. package/out/renderer/assets/yaml-7gp9FQUG.js +224 -0
  112. package/out/renderer/index.html +14 -0
  113. package/package.json +66 -4
  114. package/resources/skills/nubase-extension-authoring/SKILL.md +273 -0
  115. package/resources/templates/extension/package.json.template +13 -0
  116. package/resources/templates/extension/src/host.ts.template +12 -0
  117. package/resources/templates/extension/src/ui.tsx.template +26 -0
  118. package/resources/templates/extension/tsconfig.json.template +4 -0
  119. package/README.md +0 -3
@@ -0,0 +1,3240 @@
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 { accessSync, chmodSync, constants, 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 spawnHelperChecked = false;
1161
+ /**
1162
+ * npm installs node-pty's prebuilt macOS/Linux `spawn-helper` without its
1163
+ * execute bit, which makes every spawn fail with "posix_spawnp failed".
1164
+ */
1165
+ var ensureSpawnHelperExecutable = () => {
1166
+ if (spawnHelperChecked || process.platform === "win32") return;
1167
+ spawnHelperChecked = true;
1168
+ try {
1169
+ const root = dirname(createRequire(import.meta.url).resolve("node-pty/package.json"));
1170
+ const candidates = [join(root, "build", "Release", "spawn-helper"), join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper")];
1171
+ for (const file of candidates) {
1172
+ if (!existsSync(file)) continue;
1173
+ try {
1174
+ accessSync(file, constants.X_OK);
1175
+ } catch {
1176
+ chmodSync(file, 493);
1177
+ }
1178
+ }
1179
+ } catch {}
1180
+ };
1181
+ var PtyHost = class {
1182
+ ptys = /* @__PURE__ */ new Map();
1183
+ constructor(options) {
1184
+ this.options = options;
1185
+ }
1186
+ async spawn(options) {
1187
+ ensureSpawnHelperExecutable();
1188
+ const pty = await import("node-pty");
1189
+ const shell = process.env.SHELL ?? (process.platform === "win32" ? "powershell.exe" : "/bin/zsh");
1190
+ const id = randomUUID();
1191
+ const instance = pty.spawn(shell, ["-l"], {
1192
+ name: "xterm-256color",
1193
+ cols: options.cols,
1194
+ rows: options.rows,
1195
+ cwd: options.cwd ?? homedir(),
1196
+ env: {
1197
+ ...process.env,
1198
+ ...this.options.env,
1199
+ TERM_PROGRAM: "nubase",
1200
+ COLORTERM: "truecolor"
1201
+ }
1202
+ });
1203
+ instance.onData((data) => this.options.onData(id, data));
1204
+ instance.onExit(({ exitCode }) => {
1205
+ this.ptys.delete(id);
1206
+ this.options.onExit(id, exitCode);
1207
+ });
1208
+ this.ptys.set(id, instance);
1209
+ return id;
1210
+ }
1211
+ write(id, data) {
1212
+ this.ptys.get(id)?.write(data);
1213
+ }
1214
+ resize(id, cols, rows) {
1215
+ if (cols > 0 && rows > 0) this.ptys.get(id)?.resize(cols, rows);
1216
+ }
1217
+ kill(id) {
1218
+ this.ptys.get(id)?.kill();
1219
+ this.ptys.delete(id);
1220
+ }
1221
+ dispose() {
1222
+ for (const id of [...this.ptys.keys()]) this.kill(id);
1223
+ }
1224
+ };
1225
+ //#endregion
1226
+ //#region src/builtin-extensions/terminal/host.ts
1227
+ /** Shells for the terminal widget. Uses only the public host API plus node-pty. */
1228
+ var createTerminalHost = (env) => defineHostExtension({ activate(ctx) {
1229
+ const ptys = new PtyHost({
1230
+ env,
1231
+ onData: (id, data) => ctx.rpc.emit("data", {
1232
+ id,
1233
+ data
1234
+ }),
1235
+ onExit: (id, exitCode) => ctx.rpc.emit("exit", {
1236
+ id,
1237
+ exitCode
1238
+ })
1239
+ });
1240
+ return [
1241
+ ctx.rpc.handle("spawn", (options) => ptys.spawn(options)),
1242
+ ctx.rpc.handle("write", (id, data) => ptys.write(id, data)),
1243
+ ctx.rpc.handle("resize", (id, cols, rows) => ptys.resize(id, cols, rows)),
1244
+ ctx.rpc.handle("kill", (id) => ptys.kill(id)),
1245
+ { dispose: () => ptys.dispose() }
1246
+ ];
1247
+ } });
1248
+ //#endregion
1249
+ //#region src/main/rpc/electronTransport.ts
1250
+ var handlers = /* @__PURE__ */ new Map();
1251
+ var installed = false;
1252
+ /** One IPC handler for every view; each message goes to its sender's server. */
1253
+ var installRouter = () => {
1254
+ if (installed) return;
1255
+ installed = true;
1256
+ ipcMain.handle(INVOKE_CHANNEL, (event, method, args) => {
1257
+ const handler = handlers.get(event.sender);
1258
+ if (!handler) throw new Error("Rejected IPC from an unknown sender");
1259
+ return handler(method, args);
1260
+ });
1261
+ };
1262
+ var createElectronTransport = (contents) => {
1263
+ installRouter();
1264
+ contents.once("destroyed", () => handlers.delete(contents));
1265
+ return {
1266
+ onInvoke(handler) {
1267
+ handlers.set(contents, handler);
1268
+ },
1269
+ send(event, payload) {
1270
+ if (!contents.isDestroyed()) contents.send(EVENT_CHANNEL, event, payload);
1271
+ }
1272
+ };
1273
+ };
1274
+ //#endregion
1275
+ //#region src/main/rpc/HostServer.ts
1276
+ var HostServer = class {
1277
+ handlers = {};
1278
+ pending = /* @__PURE__ */ new Map();
1279
+ transport;
1280
+ constructor() {
1281
+ this.handlers["renderer.respond"] = (requestId, result) => {
1282
+ const request = this.pending.get(requestId);
1283
+ if (!request) return;
1284
+ this.pending.delete(requestId);
1285
+ clearTimeout(request.timer);
1286
+ if (result.ok) request.resolve(result.value);
1287
+ else request.reject(new Error(result.error));
1288
+ };
1289
+ }
1290
+ connect(transport) {
1291
+ this.transport = transport;
1292
+ transport.onInvoke(async (method, args) => {
1293
+ const handler = this.handlers[method];
1294
+ if (!handler) throw new Error(`Unknown host method: ${method}`);
1295
+ return handler(...args);
1296
+ });
1297
+ }
1298
+ handle(handlers) {
1299
+ Object.assign(this.handlers, handlers);
1300
+ }
1301
+ emit(event, payload) {
1302
+ this.transport?.send(event, payload);
1303
+ }
1304
+ /** Asks the UI to do something only it can do (layout, dialogs, commands). */
1305
+ requestRenderer(method, ...args) {
1306
+ if (!this.transport) return Promise.reject(/* @__PURE__ */ new Error("No UI is connected"));
1307
+ const requestId = randomUUID();
1308
+ const interactive = method.startsWith("dialogs.");
1309
+ return new Promise((resolve, reject) => {
1310
+ const timer = setTimeout(() => {
1311
+ this.pending.delete(requestId);
1312
+ reject(/* @__PURE__ */ new Error(`UI did not answer ${method} in time`));
1313
+ }, interactive ? 10 * 6e4 : 15e3);
1314
+ this.pending.set(requestId, {
1315
+ resolve,
1316
+ reject,
1317
+ timer
1318
+ });
1319
+ this.emit("renderer.request", {
1320
+ requestId,
1321
+ method,
1322
+ args
1323
+ });
1324
+ });
1325
+ }
1326
+ };
1327
+ //#endregion
1328
+ //#region src/main/services/agent/harness/AgentHarness.ts
1329
+ /** Thrown when a call needs a runtime or session that is not up yet. */
1330
+ var HarnessNotReadyError = class extends Error {
1331
+ constructor() {
1332
+ super("The agent is still starting");
1333
+ }
1334
+ };
1335
+ //#endregion
1336
+ //#region src/main/services/agent/Transcript.ts
1337
+ /**
1338
+ * Projects a harness's event stream into a serializable list of ChatMessages.
1339
+ * The host owns this so every UI (and a future remote UI) renders the same
1340
+ * thing, whichever harness runs the agent.
1341
+ */
1342
+ var Transcript = class {
1343
+ messages = [];
1344
+ toolParts = /* @__PURE__ */ new Map();
1345
+ counter = 0;
1346
+ constructor(onMessageChanged) {
1347
+ this.onMessageChanged = onMessageChanged;
1348
+ }
1349
+ get all() {
1350
+ return this.messages;
1351
+ }
1352
+ reset(history) {
1353
+ this.messages = [];
1354
+ this.toolParts.clear();
1355
+ for (const message of history) if (message.role === "toolResult") this.applyToolResult(message.toolCallId, message.output, message.isError, void 0, false);
1356
+ else this.startMessage(message, false);
1357
+ }
1358
+ apply(event) {
1359
+ switch (event.type) {
1360
+ case "message_start":
1361
+ if (event.message.role !== "toolResult") this.startMessage(event.message, true);
1362
+ break;
1363
+ case "message_update": {
1364
+ const { message } = event;
1365
+ if (message.role === "toolResult") break;
1366
+ const current = this.messages.at(-1);
1367
+ if (current && current.role === message.role) {
1368
+ this.fillParts(current, message);
1369
+ this.onMessageChanged(current);
1370
+ }
1371
+ break;
1372
+ }
1373
+ case "tool_start":
1374
+ this.updateTool(event.toolCallId, (part) => {
1375
+ part.status = "running";
1376
+ part.args = event.args ?? part.args;
1377
+ });
1378
+ break;
1379
+ case "tool_update":
1380
+ this.updateTool(event.toolCallId, (part) => {
1381
+ part.output = event.output ?? part.output;
1382
+ });
1383
+ break;
1384
+ case "tool_end":
1385
+ this.applyToolResult(event.toolCallId, event.output, event.isError, event.durationMs, true);
1386
+ break;
1387
+ }
1388
+ }
1389
+ startMessage(message, notify) {
1390
+ const chat = {
1391
+ id: `m${this.counter++}`,
1392
+ role: message.role,
1393
+ parts: [],
1394
+ timestamp: message.timestamp ?? Date.now()
1395
+ };
1396
+ this.fillParts(chat, message);
1397
+ this.messages.push(chat);
1398
+ if (notify) this.onMessageChanged(chat);
1399
+ }
1400
+ fillParts(chat, message) {
1401
+ chat.parts = message.content.map((item) => {
1402
+ if (item.type !== "toolCall") return item;
1403
+ const existing = this.toolParts.get(item.id);
1404
+ if (existing) {
1405
+ existing.part.args = item.arguments;
1406
+ existing.message = chat;
1407
+ return existing.part;
1408
+ }
1409
+ const part = {
1410
+ type: "toolCall",
1411
+ toolCallId: item.id,
1412
+ toolName: item.name,
1413
+ args: item.arguments,
1414
+ status: "pending"
1415
+ };
1416
+ this.toolParts.set(item.id, {
1417
+ part,
1418
+ message: chat
1419
+ });
1420
+ return part;
1421
+ });
1422
+ if (message.error) chat.error = message.error;
1423
+ }
1424
+ applyToolResult(toolCallId, output, isError, durationMs, notify) {
1425
+ const entry = this.toolParts.get(toolCallId);
1426
+ if (!entry) return;
1427
+ entry.part.status = isError ? "error" : "done";
1428
+ entry.part.output = output ?? entry.part.output;
1429
+ entry.part.durationMs = durationMs ?? entry.part.durationMs;
1430
+ if (notify) this.onMessageChanged(entry.message);
1431
+ }
1432
+ updateTool(toolCallId, update) {
1433
+ const entry = this.toolParts.get(toolCallId);
1434
+ if (!entry) return;
1435
+ update(entry.part);
1436
+ this.onMessageChanged(entry.message);
1437
+ }
1438
+ };
1439
+ //#endregion
1440
+ //#region src/main/services/agent/AgentHost.ts
1441
+ /**
1442
+ * Exposes an agent through a small, serializable API, whichever harness runs
1443
+ * it. Owns the state, the transcript and the tools Nubase extensions
1444
+ * contribute; the harness only drives the agent loop.
1445
+ */
1446
+ var AgentHost = class {
1447
+ harness;
1448
+ transcript;
1449
+ tools = /* @__PURE__ */ new Map();
1450
+ toolsDirty = false;
1451
+ sessionOpen = false;
1452
+ state;
1453
+ pendingMessages = /* @__PURE__ */ new Map();
1454
+ flushTimer;
1455
+ constructor(options) {
1456
+ this.options = options;
1457
+ this.state = {
1458
+ status: "starting",
1459
+ cwd: options.cwd
1460
+ };
1461
+ this.transcript = new Transcript((message) => this.queueMessage(message));
1462
+ this.harness = options.createHarness((event) => this.handleHarnessEvent(event));
1463
+ }
1464
+ async start() {
1465
+ try {
1466
+ this.sessionOpened(await this.harness.start());
1467
+ await this.applyPendingToolChanges();
1468
+ } catch (error) {
1469
+ this.setState({
1470
+ status: "error",
1471
+ error: errorMessage(error)
1472
+ });
1473
+ }
1474
+ }
1475
+ getState() {
1476
+ return this.state;
1477
+ }
1478
+ getMessages() {
1479
+ return this.transcript.all;
1480
+ }
1481
+ registerTool(owner, tool) {
1482
+ const entry = {
1483
+ owner,
1484
+ tool
1485
+ };
1486
+ this.tools.set(tool.name, entry);
1487
+ this.markToolsDirty();
1488
+ return { dispose: () => {
1489
+ if (this.tools.get(tool.name) !== entry) return;
1490
+ this.tools.delete(tool.name);
1491
+ this.markToolsDirty();
1492
+ } };
1493
+ }
1494
+ async prompt(text, images) {
1495
+ this.requireSession();
1496
+ if (this.harness.isRunning()) {
1497
+ await this.ready(() => this.harness.send(text, images, "followUp"));
1498
+ return;
1499
+ }
1500
+ await this.applyPendingToolChanges();
1501
+ this.harness.send(text, images, "start").catch((error) => {
1502
+ this.options.onEvent({
1503
+ type: "notification",
1504
+ message: errorMessage(error),
1505
+ level: "error"
1506
+ });
1507
+ this.setState({ status: "idle" });
1508
+ });
1509
+ }
1510
+ async steer(text, images) {
1511
+ this.requireSession();
1512
+ if (this.harness.isRunning()) await this.ready(() => this.harness.send(text, images, "steer"));
1513
+ else await this.prompt(text, images);
1514
+ }
1515
+ async abort() {
1516
+ await this.harness.abort();
1517
+ }
1518
+ async newSession() {
1519
+ await this.openSession({ kind: "new" });
1520
+ }
1521
+ async switchSession(path) {
1522
+ await this.openSession({
1523
+ kind: "open",
1524
+ path
1525
+ });
1526
+ }
1527
+ listSessions() {
1528
+ return this.harness.listSessions();
1529
+ }
1530
+ listModels() {
1531
+ return this.harness.listModels();
1532
+ }
1533
+ async setModel(provider, id) {
1534
+ await this.ready(() => this.harness.setModel(provider, id));
1535
+ this.syncState();
1536
+ }
1537
+ listProviders() {
1538
+ return this.harness.listProviders();
1539
+ }
1540
+ async login(providerId, method) {
1541
+ await this.ready(() => this.harness.login(providerId, method));
1542
+ if (!this.sessionOpen) await this.start();
1543
+ this.syncState();
1544
+ }
1545
+ async logout(providerId) {
1546
+ await this.harness.logout(providerId);
1547
+ this.syncState();
1548
+ }
1549
+ dispose() {
1550
+ this.harness.dispose();
1551
+ }
1552
+ handleHarnessEvent(event) {
1553
+ this.transcript.apply(event);
1554
+ switch (event.type) {
1555
+ case "run_start":
1556
+ this.setState({ status: "running" });
1557
+ break;
1558
+ case "run_settled":
1559
+ this.setState({ status: "idle" });
1560
+ this.applyPendingToolChanges();
1561
+ break;
1562
+ case "notice":
1563
+ this.options.onEvent({
1564
+ type: "notification",
1565
+ message: event.message,
1566
+ level: event.level
1567
+ });
1568
+ break;
1569
+ }
1570
+ }
1571
+ async openSession(ref) {
1572
+ this.sessionOpened(await this.harness.openSession(ref));
1573
+ await this.applyPendingToolChanges();
1574
+ }
1575
+ sessionOpened(history) {
1576
+ this.sessionOpen = true;
1577
+ this.transcript.reset(history);
1578
+ this.options.onEvent({
1579
+ type: "messages",
1580
+ messages: this.transcript.all
1581
+ });
1582
+ this.syncState("idle");
1583
+ }
1584
+ markToolsDirty() {
1585
+ this.toolsDirty = true;
1586
+ if (this.sessionOpen && !this.harness.isRunning()) this.applyPendingToolChanges();
1587
+ }
1588
+ /** Changing tools mid-run would disturb the agent, so changes wait for idle. */
1589
+ async applyPendingToolChanges() {
1590
+ if (!this.toolsDirty || !this.sessionOpen || this.harness.isRunning()) return;
1591
+ this.toolsDirty = false;
1592
+ try {
1593
+ await this.harness.setTools([...this.tools.values()].map((t) => t.tool));
1594
+ } catch (error) {
1595
+ this.options.onEvent({
1596
+ type: "notification",
1597
+ message: `Reloading agent tools failed: ${errorMessage(error)}`,
1598
+ level: "error"
1599
+ });
1600
+ }
1601
+ }
1602
+ syncState(status) {
1603
+ const info = this.harness.describe();
1604
+ this.setState({
1605
+ status: status ?? this.state.status,
1606
+ sessionId: info.sessionId,
1607
+ sessionFile: info.sessionFile,
1608
+ thinkingLevel: info.thinkingLevel,
1609
+ model: info.model,
1610
+ error: void 0
1611
+ });
1612
+ }
1613
+ setState(patch) {
1614
+ this.state = {
1615
+ ...this.state,
1616
+ ...patch
1617
+ };
1618
+ this.options.onEvent({
1619
+ type: "state",
1620
+ state: this.state
1621
+ });
1622
+ }
1623
+ queueMessage(message) {
1624
+ this.pendingMessages.set(message.id, message);
1625
+ if (this.flushTimer) return;
1626
+ this.flushTimer = setTimeout(() => {
1627
+ this.flushTimer = void 0;
1628
+ for (const pending of this.pendingMessages.values()) this.options.onEvent({
1629
+ type: "message",
1630
+ message: structuredClone(pending)
1631
+ });
1632
+ this.pendingMessages.clear();
1633
+ }, 33);
1634
+ }
1635
+ requireSession() {
1636
+ if (!this.sessionOpen) throw this.notReady();
1637
+ }
1638
+ /** Reports "not ready" with the startup error, when there is one. */
1639
+ async ready(call) {
1640
+ try {
1641
+ return await call();
1642
+ } catch (error) {
1643
+ if (error instanceof HarnessNotReadyError) throw this.notReady();
1644
+ throw error;
1645
+ }
1646
+ }
1647
+ notReady() {
1648
+ return new Error(this.state.error ?? "The agent is still starting");
1649
+ }
1650
+ };
1651
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1652
+ //#endregion
1653
+ //#region src/main/services/agent/harness/pi/piEvents.ts
1654
+ var resultText = (result) => {
1655
+ if (typeof result === "string") return result;
1656
+ const content = result?.content;
1657
+ if (typeof content === "string") return content;
1658
+ if (!Array.isArray(content)) return void 0;
1659
+ return content.map((part) => part.type === "text" ? part.text ?? "" : "[image]").join("\n");
1660
+ };
1661
+ var toContent = (content) => (typeof content === "string" ? [{
1662
+ type: "text",
1663
+ text: content
1664
+ }] : content ?? []).flatMap((item) => {
1665
+ switch (item.type) {
1666
+ case "text": return [{
1667
+ type: "text",
1668
+ text: item.text
1669
+ }];
1670
+ case "thinking": return [{
1671
+ type: "thinking",
1672
+ text: item.thinking
1673
+ }];
1674
+ case "image": return [{
1675
+ type: "image",
1676
+ data: item.data,
1677
+ mimeType: item.mimeType
1678
+ }];
1679
+ case "toolCall": return [{
1680
+ type: "toolCall",
1681
+ id: item.id,
1682
+ name: item.name,
1683
+ arguments: item.arguments
1684
+ }];
1685
+ default: return [];
1686
+ }
1687
+ });
1688
+ /** Pi's custom roles (bash executions, compaction summaries, …) are not shown. */
1689
+ var toNeutralMessage = (message) => {
1690
+ if (message.role === "toolResult") {
1691
+ if (!message.toolCallId) return void 0;
1692
+ return {
1693
+ role: "toolResult",
1694
+ toolCallId: message.toolCallId,
1695
+ output: resultText(message),
1696
+ isError: message.isError
1697
+ };
1698
+ }
1699
+ if (message.role !== "user" && message.role !== "assistant") return void 0;
1700
+ return {
1701
+ role: message.role,
1702
+ content: toContent(message.content),
1703
+ timestamp: message.timestamp,
1704
+ error: message.stopReason === "error" && message.errorMessage ? message.errorMessage : void 0
1705
+ };
1706
+ };
1707
+ /** Translates one pi AgentSessionEvent; returns undefined for events the UI ignores. */
1708
+ var toHarnessEvent = (event) => {
1709
+ switch (event.type) {
1710
+ case "agent_start": return { type: "run_start" };
1711
+ case "agent_settled": return { type: "run_settled" };
1712
+ case "auto_retry_start": return {
1713
+ type: "notice",
1714
+ message: `Retrying (${event.attempt}/${event.maxAttempts}): ${event.errorMessage}`,
1715
+ level: "warning"
1716
+ };
1717
+ case "message_start":
1718
+ case "message_update":
1719
+ case "message_end": {
1720
+ const message = toNeutralMessage(event.message);
1721
+ if (!message || message.role === "toolResult") return void 0;
1722
+ return {
1723
+ type: event.type === "message_start" ? "message_start" : "message_update",
1724
+ message
1725
+ };
1726
+ }
1727
+ case "tool_execution_start": return {
1728
+ type: "tool_start",
1729
+ toolCallId: event.toolCallId,
1730
+ args: event.args
1731
+ };
1732
+ case "tool_execution_update": return {
1733
+ type: "tool_update",
1734
+ toolCallId: event.toolCallId,
1735
+ output: resultText(event.partialResult)
1736
+ };
1737
+ case "tool_execution_end": return {
1738
+ type: "tool_end",
1739
+ toolCallId: event.toolCallId,
1740
+ output: resultText(event.result),
1741
+ isError: Boolean(event.isError),
1742
+ durationMs: event.durationMs
1743
+ };
1744
+ default: return;
1745
+ }
1746
+ };
1747
+ //#endregion
1748
+ //#region src/main/services/agent/harness/pi/PiHarness.ts
1749
+ /**
1750
+ * Runs the pi agent in-process. Tools contributed by Nubase are bridged into
1751
+ * pi through one inline pi extension that is re-run whenever they change.
1752
+ */
1753
+ var PiHarness = class {
1754
+ id = "pi";
1755
+ capabilities = {
1756
+ steer: true,
1757
+ multiProvider: true,
1758
+ accountLogin: true
1759
+ };
1760
+ session;
1761
+ modelRuntime;
1762
+ settingsManager;
1763
+ resourceLoader;
1764
+ unsubscribe;
1765
+ tools = [];
1766
+ constructor(options) {
1767
+ this.options = options;
1768
+ mkdirSync(options.agentDir, { recursive: true });
1769
+ }
1770
+ get sessionsDir() {
1771
+ return join(this.options.agentDir, "sessions");
1772
+ }
1773
+ async start() {
1774
+ const { agentDir, cwd } = this.options;
1775
+ this.modelRuntime = await ModelRuntime.create({
1776
+ authPath: join(agentDir, "auth.json"),
1777
+ modelsPath: join(agentDir, "models.json")
1778
+ });
1779
+ this.settingsManager = SettingsManager.create(cwd, agentDir);
1780
+ this.resourceLoader = new DefaultResourceLoader({
1781
+ cwd,
1782
+ agentDir,
1783
+ settingsManager: this.settingsManager,
1784
+ additionalSkillPaths: this.options.skillPaths,
1785
+ extensionFactories: [{
1786
+ name: "nubase",
1787
+ factory: this.bridgeExtension
1788
+ }],
1789
+ appendSystemPrompt: [this.options.appendSystemPrompt]
1790
+ });
1791
+ await this.resourceLoader.reload();
1792
+ return this.openSession({ kind: "recent" });
1793
+ }
1794
+ async openSession(ref) {
1795
+ const { cwd } = this.options;
1796
+ const sessionManager = ref.kind === "recent" ? SessionManager.continueRecent(cwd, this.sessionsDir) : ref.kind === "new" ? SessionManager.create(cwd, this.sessionsDir) : SessionManager.open(ref.path, this.sessionsDir);
1797
+ return this.openWith(sessionManager);
1798
+ }
1799
+ async send(text, images, mode) {
1800
+ const session = this.requireSession();
1801
+ const imageContent = images?.length ? images.map((image) => ({
1802
+ type: "image",
1803
+ data: image.data,
1804
+ mimeType: image.mimeType
1805
+ })) : void 0;
1806
+ await session.prompt(text, {
1807
+ images: imageContent,
1808
+ ...mode === "start" ? {} : { streamingBehavior: mode }
1809
+ });
1810
+ }
1811
+ async abort() {
1812
+ await this.session?.abort();
1813
+ }
1814
+ isRunning() {
1815
+ return Boolean(this.session?.isStreaming);
1816
+ }
1817
+ async setTools(tools) {
1818
+ this.tools = tools;
1819
+ await this.session?.reload();
1820
+ }
1821
+ describe() {
1822
+ const session = this.session;
1823
+ const model = this.hasUsableModel() ? session?.model : void 0;
1824
+ return {
1825
+ sessionId: session?.sessionId,
1826
+ sessionFile: session?.sessionFile,
1827
+ thinkingLevel: session?.thinkingLevel,
1828
+ model: model ? {
1829
+ provider: model.provider,
1830
+ id: model.id,
1831
+ name: model.name ?? model.id
1832
+ } : void 0
1833
+ };
1834
+ }
1835
+ async listSessions() {
1836
+ return (await SessionManager.list(this.options.cwd, this.sessionsDir)).map((s) => ({
1837
+ id: s.id,
1838
+ path: s.path,
1839
+ name: s.name,
1840
+ firstMessage: s.firstMessage,
1841
+ modified: s.modified.getTime(),
1842
+ messageCount: s.messageCount
1843
+ })).sort((a, b) => b.modified - a.modified);
1844
+ }
1845
+ async listModels() {
1846
+ if (!this.modelRuntime) return [];
1847
+ return (await this.modelRuntime.getAvailable()).map((m) => ({
1848
+ provider: m.provider,
1849
+ id: m.id,
1850
+ name: m.name ?? m.id
1851
+ }));
1852
+ }
1853
+ async setModel(provider, id) {
1854
+ const model = this.modelRuntime?.getModel(provider, id);
1855
+ if (!model) throw new Error(`Unknown model ${provider}/${id}`);
1856
+ await this.requireSession().setModel(model);
1857
+ }
1858
+ /** Providers the user can connect, with how they sign in and whether they are ready. */
1859
+ listProviders() {
1860
+ const runtime = this.modelRuntime;
1861
+ if (!runtime) return [];
1862
+ return runtime.getProviders().map((provider) => {
1863
+ const status = runtime.getProviderAuthStatus(provider.id);
1864
+ return {
1865
+ id: provider.id,
1866
+ name: provider.name,
1867
+ configured: status.configured,
1868
+ source: status.source,
1869
+ accountLogin: provider.auth.oauth ? {
1870
+ name: provider.auth.oauth.name,
1871
+ subscription: Boolean(provider.auth.oauth.isSubscription)
1872
+ } : void 0,
1873
+ apiKeyLogin: Boolean(provider.auth.apiKey?.login)
1874
+ };
1875
+ }).filter((p) => p.accountLogin || p.apiKeyLogin || p.configured).sort((a, b) => a.name.localeCompare(b.name));
1876
+ }
1877
+ /** Runs pi's own sign-in flow for a provider, with Nubase dialogs as the UI. */
1878
+ async login(providerId, method) {
1879
+ const runtime = this.modelRuntime;
1880
+ if (!runtime) throw new HarnessNotReadyError();
1881
+ await runtime.login(providerId, method === "account" ? "oauth" : "api_key", this.createLoginInteraction());
1882
+ await runtime.refresh({ providers: [providerId] }).catch(() => void 0);
1883
+ await this.ensureUsableModel();
1884
+ }
1885
+ async logout(providerId) {
1886
+ await this.modelRuntime?.logout(providerId);
1887
+ }
1888
+ dispose() {
1889
+ this.unsubscribe?.();
1890
+ this.session?.dispose();
1891
+ }
1892
+ async openWith(sessionManager) {
1893
+ if (!this.modelRuntime || !this.resourceLoader || !this.settingsManager) throw new Error("Agent is not initialized");
1894
+ this.unsubscribe?.();
1895
+ this.session?.dispose();
1896
+ const preferred = this.options.model ? this.modelRuntime.getModel(this.options.model.provider, this.options.model.id) : void 0;
1897
+ const { session } = await createAgentSession({
1898
+ cwd: this.options.cwd,
1899
+ agentDir: this.options.agentDir,
1900
+ modelRuntime: this.modelRuntime,
1901
+ resourceLoader: this.resourceLoader,
1902
+ settingsManager: this.settingsManager,
1903
+ sessionManager,
1904
+ ...preferred ? { model: preferred } : {}
1905
+ });
1906
+ this.session = session;
1907
+ await session.bindExtensions({
1908
+ mode: "rpc",
1909
+ uiContext: this.createUiContext()
1910
+ });
1911
+ this.unsubscribe = session.subscribe((event) => {
1912
+ const translated = toHarnessEvent(event);
1913
+ if (translated) this.options.emit(translated);
1914
+ });
1915
+ await this.ensureUsableModel();
1916
+ return session.messages.flatMap((message) => {
1917
+ const neutral = toNeutralMessage(message);
1918
+ return neutral ? [neutral] : [];
1919
+ });
1920
+ }
1921
+ bridgeExtension = (pi) => {
1922
+ for (const tool of this.tools) pi.registerTool({
1923
+ name: tool.name,
1924
+ label: tool.label ?? tool.name,
1925
+ description: tool.description,
1926
+ parameters: tool.parameters,
1927
+ execute: async (_toolCallId, params, signal, onUpdate) => {
1928
+ const raw = await tool.execute(params, {
1929
+ signal,
1930
+ update: (text) => onUpdate?.({
1931
+ content: [{
1932
+ type: "text",
1933
+ text
1934
+ }],
1935
+ details: void 0
1936
+ })
1937
+ });
1938
+ const result = typeof raw === "string" ? { text: raw } : raw;
1939
+ if (result.isError) throw new Error(result.text);
1940
+ return {
1941
+ content: [{
1942
+ type: "text",
1943
+ text: result.text
1944
+ }, ...(result.images ?? []).map((image) => ({
1945
+ type: "image",
1946
+ data: image.data,
1947
+ mimeType: image.mimeType
1948
+ }))],
1949
+ details: result.details ?? {}
1950
+ };
1951
+ }
1952
+ });
1953
+ };
1954
+ createLoginInteraction() {
1955
+ const { dialogs } = this.options;
1956
+ const cancelled = () => /* @__PURE__ */ new Error("Login cancelled");
1957
+ return {
1958
+ prompt: async (prompt) => {
1959
+ if (prompt.type === "select") {
1960
+ const labels = prompt.options.map((o) => o.description ? `${o.label} — ${o.description}` : o.label);
1961
+ const choice = await dialogs.select(prompt.message, labels);
1962
+ const option = prompt.options[labels.indexOf(choice ?? "")];
1963
+ if (!option) throw cancelled();
1964
+ return option.id;
1965
+ }
1966
+ const value = await dialogs.input(prompt.message, {
1967
+ placeholder: prompt.placeholder,
1968
+ secret: prompt.type === "secret",
1969
+ signal: prompt.signal
1970
+ });
1971
+ if (value === void 0) throw cancelled();
1972
+ return value;
1973
+ },
1974
+ notify: (event) => {
1975
+ switch (event.type) {
1976
+ case "auth_url":
1977
+ dialogs.openExternal(event.url);
1978
+ dialogs.notify(event.instructions ?? "Continue signing in in your browser.", "info");
1979
+ break;
1980
+ case "device_code":
1981
+ dialogs.openExternal(event.verificationUri);
1982
+ dialogs.confirm(`Enter this code in your browser: ${event.userCode}`, `If the page did not open, go to ${event.verificationUri}`);
1983
+ break;
1984
+ case "info":
1985
+ case "progress":
1986
+ dialogs.notify(event.message, "info");
1987
+ break;
1988
+ }
1989
+ }
1990
+ };
1991
+ }
1992
+ /** pi extension UI requests, answered with Nubase dialogs. */
1993
+ createUiContext() {
1994
+ const { dialogs } = this.options;
1995
+ return new Proxy({
1996
+ select: (title, options) => dialogs.select(title, options),
1997
+ confirm: (title, message) => dialogs.confirm(title, message),
1998
+ input: (title, placeholder) => dialogs.input(title, { placeholder }),
1999
+ notify: (message, type) => dialogs.notify(message, type === "error" ? "error" : "info"),
2000
+ editor: (title, prefill) => dialogs.input(title, { placeholder: prefill })
2001
+ }, { get: (target, prop) => prop in target ? target[prop] : () => prop === "onTerminalInput" ? () => {} : void 0 });
2002
+ }
2003
+ /** pi defaults to a model even without credentials; prefer one that can actually run. */
2004
+ async ensureUsableModel() {
2005
+ const session = this.session;
2006
+ const runtime = this.modelRuntime;
2007
+ if (!session || !runtime) return;
2008
+ const current = session.model;
2009
+ if (current && runtime.hasConfiguredAuth(current.provider)) return;
2010
+ const fallback = (await runtime.getAvailable().catch(() => []))[0];
2011
+ if (fallback) await session.setModel(fallback);
2012
+ }
2013
+ hasUsableModel() {
2014
+ const model = this.session?.model;
2015
+ return Boolean(model && this.modelRuntime?.hasConfiguredAuth(model.provider));
2016
+ }
2017
+ requireSession() {
2018
+ if (!this.session) throw new HarnessNotReadyError();
2019
+ return this.session;
2020
+ }
2021
+ };
2022
+ //#endregion
2023
+ //#region src/main/services/agent/nubaseTools.ts
2024
+ var formatDiagnostics = (diagnostics) => diagnostics.map((d) => {
2025
+ const where = d.file ? `${d.file}${d.line ? `:${d.line}:${d.column ?? 1}` : ""} ` : "";
2026
+ return `- [${d.source}] ${where}${d.message}`;
2027
+ }).join("\n");
2028
+ var describe = (info) => `${info.id} (${info.displayName}) — ${info.status}${info.builtin ? ", built-in" : `, ${info.source}, ${info.path}`}`;
2029
+ var object = (properties, required = []) => ({
2030
+ type: "object",
2031
+ properties,
2032
+ required,
2033
+ additionalProperties: false
2034
+ });
2035
+ /**
2036
+ * Tools that let the agent change Nubase itself: create and build
2037
+ * extensions, arrange the dock, run UI commands, and look at the result.
2038
+ */
2039
+ var createNubaseTools = (deps) => {
2040
+ const { server, extensions } = deps;
2041
+ const buildAndLoad = async (id) => {
2042
+ const info = await extensions.rebuild(id);
2043
+ if (info.status !== "active") return [
2044
+ `Build of "${id}" FAILED (${info.status}). The previous working version (if any) is still loaded.`,
2045
+ formatDiagnostics(info.diagnostics),
2046
+ "Fix these errors and call nubase_build_extension again."
2047
+ ].join("\n");
2048
+ const lines = [`Built "${id}" successfully (build ${info.buildId}).`];
2049
+ if (info.buildId && info.uiBundleUrl) {
2050
+ const loaded = await server.requestRenderer("extensions.ensureLoaded", id, info.buildId);
2051
+ if (!loaded.ok) {
2052
+ lines.push(`But loading it in the UI failed:\n${loaded.error}`);
2053
+ return lines.join("\n");
2054
+ }
2055
+ lines.push(`Loaded in the UI. Widgets: ${loaded.widgets.join(", ") || "(none)"}. Commands: ${loaded.commands.join(", ") || "(none)"}.`);
2056
+ }
2057
+ return lines.join("\n");
2058
+ };
2059
+ return [
2060
+ {
2061
+ name: "nubase_list_extensions",
2062
+ label: "List extensions",
2063
+ 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.",
2064
+ parameters: object({}),
2065
+ execute: () => [
2066
+ `Global extensions folder: ${deps.globalExtensionsDir}`,
2067
+ deps.folderExtensionsDir ? `This folder's extensions folder: ${deps.folderExtensionsDir}` : "This is the ~ tab: only global extensions apply.",
2068
+ ...extensions.list().map((info) => describe(info) + (info.diagnostics.length ? `\n${formatDiagnostics(info.diagnostics)}` : ""))
2069
+ ].join("\n")
2070
+ },
2071
+ {
2072
+ name: "nubase_create_extension",
2073
+ label: "Create extension",
2074
+ 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.",
2075
+ parameters: object({
2076
+ id: {
2077
+ type: "string",
2078
+ description: "lowercase kebab-case, e.g. jira-tickets"
2079
+ },
2080
+ displayName: { type: "string" },
2081
+ description: { type: "string" },
2082
+ withHost: {
2083
+ type: "boolean",
2084
+ description: "Also create src/host.ts (Node side)."
2085
+ },
2086
+ scope: {
2087
+ type: "string",
2088
+ enum: ["global", "folder"],
2089
+ description: "global (default): ~/.nubase/extensions. folder: <folder>/.nubase/extensions, only for this folder."
2090
+ }
2091
+ }, ["id", "displayName"]),
2092
+ execute: async (params) => {
2093
+ const { scope = "global", ...options } = params;
2094
+ if (scope === "folder" && !deps.folderExtensionsDir) return {
2095
+ text: "The ~ tab has no folder of its own; use scope \"global\".",
2096
+ isError: true
2097
+ };
2098
+ const path = await scaffoldExtension(scope === "folder" ? await deps.ensureFolderExtensions() : deps.globalExtensionsDir, deps.templateDir, {
2099
+ ...options,
2100
+ withHost: options.withHost ?? false
2101
+ });
2102
+ const result = await buildAndLoad(params.id);
2103
+ return [
2104
+ `Created ${path}`,
2105
+ `Files: package.json, tsconfig.json, src/ui.tsx${params.withHost ? ", src/host.ts" : ""}`,
2106
+ `The starter widget id is "${params.id}.main". Edit the files, then call nubase_build_extension.`,
2107
+ result
2108
+ ].join("\n");
2109
+ }
2110
+ },
2111
+ {
2112
+ name: "nubase_build_extension",
2113
+ label: "Build extension",
2114
+ 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.",
2115
+ parameters: object({ id: { type: "string" } }, ["id"]),
2116
+ execute: async ({ id }) => buildAndLoad(id)
2117
+ },
2118
+ {
2119
+ name: "nubase_revert_extension",
2120
+ label: "Revert extension",
2121
+ 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.",
2122
+ parameters: object({ id: { type: "string" } }, ["id"]),
2123
+ execute: async ({ id }) => `Reverted "${id}" to: ${await extensions.revert(id)}`
2124
+ },
2125
+ {
2126
+ name: "nubase_extension_logs",
2127
+ label: "Extension logs",
2128
+ description: "Returns recent runtime errors and logs from extensions (render errors caught by widget error boundaries, console errors, host-side logs).",
2129
+ parameters: object({ id: {
2130
+ type: "string",
2131
+ description: "Omit for all extensions."
2132
+ } }),
2133
+ execute: ({ id }) => {
2134
+ const logs = extensions.getLogs(id).slice(-40);
2135
+ if (logs.length === 0) return "No logs.";
2136
+ 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");
2137
+ }
2138
+ },
2139
+ {
2140
+ name: "nubase_get_layout",
2141
+ label: "Get layout",
2142
+ description: "Returns the dock layout (tabs, splits and blocks with their widget ids) and the widgets that can be opened.",
2143
+ parameters: object({}),
2144
+ execute: async () => {
2145
+ const [layout, widgets] = await Promise.all([server.requestRenderer("layout.get"), server.requestRenderer("widgets.list")]);
2146
+ return [
2147
+ "Layout:",
2148
+ JSON.stringify(layout, null, 2),
2149
+ "Available widgets:",
2150
+ ...widgets.map((w) => `- ${w.id}: ${w.title}${w.description ? ` — ${w.description}` : ""} (${w.extensionId})`)
2151
+ ].join("\n");
2152
+ }
2153
+ },
2154
+ {
2155
+ name: "nubase_open_widget",
2156
+ label: "Open widget",
2157
+ description: "Opens a widget in a new dock block next to the focused one (or relative to a given block). Returns the block id.",
2158
+ parameters: object({
2159
+ widgetId: { type: "string" },
2160
+ placement: {
2161
+ type: "string",
2162
+ enum: [
2163
+ "right",
2164
+ "below",
2165
+ "left",
2166
+ "above",
2167
+ "replace"
2168
+ ]
2169
+ },
2170
+ relativeTo: {
2171
+ type: "string",
2172
+ description: "Block id; defaults to the focused block."
2173
+ },
2174
+ state: { description: "Initial widget state." }
2175
+ }, ["widgetId"]),
2176
+ execute: async (params) => {
2177
+ const blockId = await server.requestRenderer("layout.openWidget", params.widgetId, params);
2178
+ return `Opened ${params.widgetId} in block ${blockId}.`;
2179
+ }
2180
+ },
2181
+ {
2182
+ name: "nubase_close_block",
2183
+ label: "Close block",
2184
+ description: "Closes a dock block by id.",
2185
+ parameters: object({ blockId: { type: "string" } }, ["blockId"]),
2186
+ execute: async ({ blockId }) => {
2187
+ await server.requestRenderer("layout.closeBlock", blockId);
2188
+ return `Closed ${blockId}.`;
2189
+ }
2190
+ },
2191
+ {
2192
+ name: "nubase_list_commands",
2193
+ label: "List commands",
2194
+ description: "Lists UI commands (the same ones in the command palette).",
2195
+ parameters: object({}),
2196
+ execute: async () => (await server.requestRenderer("commands.list")).map((c) => `- ${c.id}: ${c.category ? `${c.category}: ` : ""}${c.title}`).join("\n")
2197
+ },
2198
+ {
2199
+ name: "nubase_run_command",
2200
+ label: "Run command",
2201
+ description: "Runs a UI command by id, with optional arguments.",
2202
+ parameters: object({
2203
+ id: { type: "string" },
2204
+ args: {}
2205
+ }, ["id"]),
2206
+ execute: async ({ id, args }) => {
2207
+ const result = await server.requestRenderer("commands.execute", id, args);
2208
+ return result === void 0 ? `Ran ${id}.` : JSON.stringify(result);
2209
+ }
2210
+ },
2211
+ {
2212
+ name: "nubase_screenshot",
2213
+ label: "Screenshot",
2214
+ 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.",
2215
+ parameters: object({ blockId: {
2216
+ type: "string",
2217
+ description: "Omit for the whole window."
2218
+ } }),
2219
+ execute: async ({ blockId }) => {
2220
+ const contents = deps.getWebContents();
2221
+ if (!contents) return {
2222
+ text: "No window is open.",
2223
+ isError: true
2224
+ };
2225
+ const bounds = blockId ? await server.requestRenderer("block.bounds", blockId) : void 0;
2226
+ if (blockId && !bounds) return {
2227
+ text: `No block ${blockId}.`,
2228
+ isError: true
2229
+ };
2230
+ let image = await contents.capturePage(bounds ? {
2231
+ x: Math.round(bounds.x),
2232
+ y: Math.round(bounds.y),
2233
+ width: Math.round(bounds.width),
2234
+ height: Math.round(bounds.height)
2235
+ } : void 0);
2236
+ if (image.getSize().width > 1400) image = image.resize({ width: 1400 });
2237
+ return {
2238
+ text: `Screenshot of ${blockId ? `block ${blockId}` : "the window"}.`,
2239
+ images: [{
2240
+ data: image.toPNG().toString("base64"),
2241
+ mimeType: "image/png"
2242
+ }]
2243
+ };
2244
+ }
2245
+ }
2246
+ ];
2247
+ };
2248
+ //#endregion
2249
+ //#region src/main/services/agent/systemPrompt.ts
2250
+ var folderSection = (options) => options.isHome || !options.folderNubaseDir ? `
2251
+ You are in the ~ tab (the user's home folder). It is configured only by
2252
+ ${options.nubaseHome}.` : `
2253
+ You are in the tab for the folder ${options.folder}. Its configuration is
2254
+ ${options.nubaseHome} with ${options.folderNubaseDir} layered on top:
2255
+ - ${options.folderNubaseDir}/settings.json overrides keys of
2256
+ ${options.nubaseHome}/settings.json for this folder only (e.g. a different
2257
+ theme). Edit the root file for changes meant for every folder.
2258
+ - ${options.folderNubaseDir}/extensions/<id> replaces the global extension with
2259
+ the same id, in this folder only.
2260
+ New extensions are global unless the user asks for one specific to this
2261
+ folder or project (then pass scope "folder" to nubase_create_extension).`;
2262
+ var nubaseSystemPrompt = (options) => `
2263
+ # Nubase
2264
+
2265
+ You are running inside Nubase, a desktop AI harness whose UI you can change.
2266
+ Each top-level tab is a folder; this conversation belongs to one of them.
2267
+ The UI is a dock of blocks; every block shows a widget. Widgets, commands,
2268
+ keybindings and themes come from extensions: TypeScript projects in
2269
+ ${options.globalExtensionsDir} (global) or in a folder's .nubase/extensions.
2270
+ Built-in widgets (chat, files, terminal, extensions, settings) use the same
2271
+ public API you do.
2272
+ ${folderSection(options)}
2273
+
2274
+ When the user asks for new UI — a widget, a dashboard, an integration with a
2275
+ service — build it as an extension:
2276
+
2277
+ 1. Read the nubase-extension-authoring skill before writing extension code.
2278
+ 2. Use nubase_create_extension (or edit an existing extension).
2279
+ 3. Edit the files with your file tools. Never edit dist/ or tsconfig.base.json.
2280
+ 4. Call nubase_build_extension and fix every reported error until it builds.
2281
+ 5. Open the widget with nubase_open_widget, then check it with nubase_screenshot.
2282
+ 6. If something renders wrong, read nubase_extension_logs and iterate.
2283
+
2284
+ Secrets (API tokens) belong in the extension's host half: call
2285
+ ctx.secrets.prompt(...) there. Never ask the user to paste secrets into chat.
2286
+ Every successful build is committed to git; nubase_revert_extension restores
2287
+ the last version that built.
2288
+ `;
2289
+ //#endregion
2290
+ //#region src/main/services/extensions/ExtensionHost.ts
2291
+ globalThis[HOST_MODULES_GLOBAL] = {
2292
+ "@nubase/sdk/host": sdkHost,
2293
+ "@nubase/sdk": sdkHost
2294
+ };
2295
+ var Scope = class {
2296
+ items = [];
2297
+ add(d) {
2298
+ this.items.push(d);
2299
+ return d;
2300
+ }
2301
+ dispose() {
2302
+ for (const d of this.items.splice(0).reverse()) try {
2303
+ d.dispose();
2304
+ } catch (error) {
2305
+ console.error("Error disposing extension resource", error);
2306
+ }
2307
+ }
2308
+ };
2309
+ var MAX_LOGS = 200;
2310
+ /**
2311
+ * The extensions one folder tab runs. Builds come from the sources (shared
2312
+ * between tabs); this activates the winning version of each id — the
2313
+ * folder's own over the global one — into this tab's agent and UI.
2314
+ */
2315
+ var ExtensionHost = class {
2316
+ records = /* @__PURE__ */ new Map();
2317
+ sources;
2318
+ logs = [];
2319
+ unsubscribe = [];
2320
+ syncing = /* @__PURE__ */ new Map();
2321
+ constructor(deps) {
2322
+ this.deps = deps;
2323
+ this.sources = [...deps.sources];
2324
+ }
2325
+ async start() {
2326
+ for (const builtin of this.deps.builtins) {
2327
+ const record = {
2328
+ builtin,
2329
+ rpc: /* @__PURE__ */ new Map(),
2330
+ info: {
2331
+ id: builtin.id,
2332
+ displayName: builtin.displayName,
2333
+ description: builtin.description,
2334
+ path: "",
2335
+ builtin: true,
2336
+ source: "builtin",
2337
+ status: "active",
2338
+ diagnostics: []
2339
+ }
2340
+ };
2341
+ this.records.set(builtin.id, record);
2342
+ if (builtin.host) await this.activateHost(record, builtin.host);
2343
+ }
2344
+ if (!this.deps.safeMode) {
2345
+ for (const source of this.sources) this.subscribe(source);
2346
+ await Promise.all(this.sources.map((source) => source.start()));
2347
+ await this.syncAll();
2348
+ }
2349
+ this.changed();
2350
+ }
2351
+ /** Adds the folder's own extensions once its `.nubase/extensions` exists. Takes precedence over the others. */
2352
+ async addSource(source) {
2353
+ if (this.sources.includes(source)) return;
2354
+ this.sources.unshift(source);
2355
+ if (this.deps.safeMode) return;
2356
+ this.subscribe(source);
2357
+ await source.start();
2358
+ await this.syncAll();
2359
+ }
2360
+ list() {
2361
+ return [...this.records.values()].map((r) => r.info);
2362
+ }
2363
+ get(id) {
2364
+ return this.records.get(id)?.info;
2365
+ }
2366
+ /** The source an id resolves to (highest precedence that has it). */
2367
+ sourceOf(id) {
2368
+ return this.sources.find((source) => source.contains(id));
2369
+ }
2370
+ pathOf(id) {
2371
+ const source = this.sourceOf(id);
2372
+ if (!source) throw new Error(`No extension "${id}"`);
2373
+ return source.pathOf(id);
2374
+ }
2375
+ /** Builds (or rebuilds) an extension and hot-reloads it if the build succeeds. */
2376
+ async rebuild(id) {
2377
+ const record = this.records.get(id);
2378
+ if (record?.builtin) return record.info;
2379
+ const source = this.sourceOf(id);
2380
+ if (!source) throw new Error(`No extension "${id}" in ${this.sources.map((s) => s.dir).join(" or ")}`);
2381
+ await source.rebuild(id);
2382
+ await this.sync(id);
2383
+ const info = this.records.get(id)?.info;
2384
+ if (!info) throw new Error(`Extension "${id}" disappeared`);
2385
+ return info;
2386
+ }
2387
+ async revert(id) {
2388
+ const source = this.sourceOf(id);
2389
+ if (!source) throw new Error(`No extension "${id}"`);
2390
+ const commit = await source.revert(id);
2391
+ await this.sync(id);
2392
+ return commit;
2393
+ }
2394
+ async setEnabled(id) {
2395
+ await this.sync(id);
2396
+ }
2397
+ /** Re-applies settings such as `disabledExtensions` to every extension. */
2398
+ async refresh() {
2399
+ await this.syncAll();
2400
+ }
2401
+ async call(id, method, args) {
2402
+ const handler = this.records.get(id)?.rpc.get(method);
2403
+ if (!handler) throw new Error(`Extension "${id}" has no host method "${method}"`);
2404
+ return handler(...args);
2405
+ }
2406
+ reportLog(entry) {
2407
+ this.logs.push(entry);
2408
+ if (this.logs.length > MAX_LOGS) this.logs.splice(0, this.logs.length - MAX_LOGS);
2409
+ }
2410
+ getLogs(id, since) {
2411
+ return this.logs.filter((l) => (!id || l.extensionId === id) && (!since || l.timestamp >= since));
2412
+ }
2413
+ dispose() {
2414
+ for (const unsubscribe of this.unsubscribe.splice(0)) unsubscribe();
2415
+ for (const record of this.records.values()) record.hostScope?.dispose();
2416
+ }
2417
+ subscribe(source) {
2418
+ this.unsubscribe.push(source.onChanged((id) => this.sync(id)));
2419
+ }
2420
+ async syncAll() {
2421
+ const ids = new Set(this.sources.flatMap((source) => source.ids()));
2422
+ for (const record of this.records.values()) if (!record.builtin) ids.add(record.info.id);
2423
+ await Promise.all([...ids].map((id) => this.sync(id)));
2424
+ }
2425
+ /** Serializes work per id: activation is async and sources notify often. */
2426
+ sync(id) {
2427
+ const next = (this.syncing.get(id) ?? Promise.resolve()).then(() => this.doSync(id));
2428
+ const tracked = next.finally(() => {
2429
+ if (this.syncing.get(id) === tracked) this.syncing.delete(id);
2430
+ });
2431
+ this.syncing.set(id, tracked);
2432
+ return next;
2433
+ }
2434
+ async doSync(id) {
2435
+ const existing = this.records.get(id);
2436
+ if (existing?.builtin) return;
2437
+ const source = this.sources.find((s) => s.get(id));
2438
+ const entry = source?.get(id);
2439
+ if (!source || !entry) {
2440
+ if (existing) {
2441
+ existing.hostScope?.dispose();
2442
+ this.records.delete(id);
2443
+ this.changed();
2444
+ }
2445
+ return;
2446
+ }
2447
+ const record = existing ?? {
2448
+ rpc: /* @__PURE__ */ new Map(),
2449
+ info: source.toInfo(entry)
2450
+ };
2451
+ this.records.set(id, record);
2452
+ if (record.source !== source) {
2453
+ this.deactivateHost(record);
2454
+ record.source = source;
2455
+ }
2456
+ const info = source.toInfo(entry);
2457
+ if (this.deps.isDisabled(id)) {
2458
+ this.deactivateHost(record);
2459
+ record.info = {
2460
+ ...info,
2461
+ status: "disabled",
2462
+ uiBundleUrl: void 0,
2463
+ uiStylesUrl: void 0
2464
+ };
2465
+ this.changed();
2466
+ return;
2467
+ }
2468
+ if (entry.status !== "active" || !entry.buildId) {
2469
+ record.info = info;
2470
+ this.changed();
2471
+ return;
2472
+ }
2473
+ if (!entry.hasHost) this.deactivateHost(record);
2474
+ else if (record.activeBuildId !== entry.buildId || !record.hostScope) {
2475
+ this.deactivateHost(record);
2476
+ try {
2477
+ await this.activateHost(record, source.loadHost(id));
2478
+ } catch (error) {
2479
+ record.info = {
2480
+ ...info,
2481
+ status: "activation-failed",
2482
+ diagnostics: [{
2483
+ source: "activation",
2484
+ message: errorText(error)
2485
+ }]
2486
+ };
2487
+ this.changed();
2488
+ return;
2489
+ }
2490
+ }
2491
+ record.activeBuildId = entry.buildId;
2492
+ record.info = info;
2493
+ this.changed();
2494
+ }
2495
+ deactivateHost(record) {
2496
+ record.hostScope?.dispose();
2497
+ record.hostScope = void 0;
2498
+ record.activeBuildId = void 0;
2499
+ record.rpc.clear();
2500
+ }
2501
+ async activateHost(record, extension) {
2502
+ const scope = new Scope();
2503
+ const id = record.info.id;
2504
+ const log = (level) => (...args) => {
2505
+ const message = args.map((a) => typeof a === "string" ? a : safeJson(a)).join(" ");
2506
+ this.reportLog({
2507
+ extensionId: id,
2508
+ level,
2509
+ message,
2510
+ timestamp: Date.now()
2511
+ });
2512
+ console[level === "info" ? "log" : level](`[${id}]`, ...args);
2513
+ };
2514
+ const ctx = {
2515
+ extension: {
2516
+ id,
2517
+ displayName: record.info.displayName,
2518
+ path: record.info.path
2519
+ },
2520
+ workspace: this.deps.workspace,
2521
+ agent: { registerTool: (tool) => scope.add(this.deps.agent.registerTool(id, tool)) },
2522
+ rpc: {
2523
+ handle: (method, handler) => {
2524
+ record.rpc.set(method, handler);
2525
+ return scope.add({ dispose: () => {
2526
+ if (record.rpc.get(method) === handler) record.rpc.delete(method);
2527
+ } });
2528
+ },
2529
+ emit: (event, payload) => this.deps.onEvent(id, event, payload)
2530
+ },
2531
+ secrets: {
2532
+ get: (key) => this.deps.secrets.get(`ext.${id}.${key}`),
2533
+ set: (key, value) => this.deps.secrets.set(`ext.${id}.${key}`, value),
2534
+ delete: (key) => this.deps.secrets.delete(`ext.${id}.${key}`),
2535
+ prompt: async (key, title) => {
2536
+ const value = await this.deps.promptSecret(title);
2537
+ if (value) await this.deps.secrets.set(`ext.${id}.${key}`, value);
2538
+ return value;
2539
+ }
2540
+ },
2541
+ storage: {
2542
+ get: async (key) => await this.deps.storage.get(id, key),
2543
+ set: (key, value) => this.deps.storage.set(id, key, value)
2544
+ },
2545
+ fs: this.deps.fs,
2546
+ log: {
2547
+ info: log("info"),
2548
+ warn: log("warn"),
2549
+ error: log("error")
2550
+ }
2551
+ };
2552
+ const returned = await extension.activate(ctx);
2553
+ for (const disposable of returned ?? []) scope.add(disposable);
2554
+ record.hostScope = scope;
2555
+ }
2556
+ changed() {
2557
+ this.deps.onChanged(this.list());
2558
+ }
2559
+ };
2560
+ var errorText = (error) => error instanceof Error ? error.stack ?? error.message : String(error);
2561
+ var safeJson = (value) => {
2562
+ try {
2563
+ return JSON.stringify(value);
2564
+ } catch {
2565
+ return String(value);
2566
+ }
2567
+ };
2568
+ //#endregion
2569
+ //#region src/main/services/store/LayeredSettings.ts
2570
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2571
+ /**
2572
+ * Folder settings win: objects merge key by key, keybindings add to the
2573
+ * root ones (registered later, so they take precedence), and any other
2574
+ * value — arrays included — replaces the root's.
2575
+ */
2576
+ var mergeSettings = (root, folder) => {
2577
+ const merge = (base, over) => {
2578
+ const result = { ...base };
2579
+ for (const [key, value] of Object.entries(over)) {
2580
+ const current = result[key];
2581
+ if (key === "keybindings" && Array.isArray(current)) result[key] = [...current, ...Array.isArray(value) ? value : []];
2582
+ else if (isPlainObject(current) && isPlainObject(value)) result[key] = merge(current, value);
2583
+ else if (value !== void 0) result[key] = value;
2584
+ }
2585
+ return result;
2586
+ };
2587
+ return merge(root, folder);
2588
+ };
2589
+ /** The settings one folder tab sees: `~/.nubase/settings.json` with the folder's `.nubase/settings.json` on top. */
2590
+ var LayeredSettings = class {
2591
+ folder = {};
2592
+ folderFile;
2593
+ listeners = /* @__PURE__ */ new Set();
2594
+ disposers = [];
2595
+ constructor(root, folderSettingsPath) {
2596
+ this.root = root;
2597
+ if (folderSettingsPath) this.folderFile = new JsonFile(folderSettingsPath);
2598
+ }
2599
+ async load() {
2600
+ this.disposers.push(this.root.onChange(() => this.notify()));
2601
+ if (!this.folderFile) return;
2602
+ this.folder = await this.folderFile.read() ?? {};
2603
+ this.disposers.push(this.folderFile.onExternalChange((value) => {
2604
+ this.folder = isPlainObject(value) ? value : {};
2605
+ this.notify();
2606
+ }, { createDir: false }));
2607
+ }
2608
+ get() {
2609
+ return mergeSettings(this.root.get(), this.folder);
2610
+ }
2611
+ getFolderOverrides() {
2612
+ return this.folder;
2613
+ }
2614
+ async update(patch, scope = "root") {
2615
+ if (scope === "root") {
2616
+ await this.root.update(patch);
2617
+ return this.get();
2618
+ }
2619
+ if (!this.folderFile) throw new Error("The ~ tab has no folder settings; use the root scope");
2620
+ this.folder = {
2621
+ ...this.folder,
2622
+ ...patch
2623
+ };
2624
+ await this.folderFile.write(this.folder);
2625
+ this.notify();
2626
+ return this.get();
2627
+ }
2628
+ onChange(listener) {
2629
+ this.listeners.add(listener);
2630
+ return () => this.listeners.delete(listener);
2631
+ }
2632
+ dispose() {
2633
+ for (const dispose of this.disposers.splice(0)) dispose();
2634
+ this.listeners.clear();
2635
+ }
2636
+ notify() {
2637
+ const value = this.get();
2638
+ for (const listener of this.listeners) listener(value);
2639
+ }
2640
+ };
2641
+ //#endregion
2642
+ //#region src/main/services/workspaces/Workspace.ts
2643
+ /** Collapses a pre-folder-tabs layout (several tabs) into its active tab. */
2644
+ var migrateLegacyLayout = async (target) => {
2645
+ if (existsSync(target.path) || !existsSync(paths.legacyLayout)) return;
2646
+ const legacy = await new JsonFile(paths.legacyLayout).read();
2647
+ const tab = legacy?.tabs?.find((t) => t.id === legacy.activeTabId) ?? legacy?.tabs?.[0];
2648
+ if (!legacy || !tab) return;
2649
+ await target.write({
2650
+ ...legacy,
2651
+ tabs: [tab],
2652
+ activeTabId: tab.id
2653
+ });
2654
+ };
2655
+ /**
2656
+ * One folder tab: its own renderer (view), host server, merged settings,
2657
+ * agent (working in the folder) and extension host. Built-ins and global
2658
+ * extensions come from `~/.nubase`; the folder's `.nubase` overrides them.
2659
+ */
2660
+ var Workspace = class {
2661
+ view;
2662
+ server = new HostServer();
2663
+ settings;
2664
+ agent;
2665
+ extensions;
2666
+ folderExtensions;
2667
+ layoutFile;
2668
+ fsWatches = /* @__PURE__ */ new Map();
2669
+ watchCounter = 0;
2670
+ disposers = [];
2671
+ constructor(id, folder, shared, tabs) {
2672
+ this.id = id;
2673
+ this.folder = folder;
2674
+ this.shared = shared;
2675
+ this.tabs = tabs;
2676
+ const isHome = this.info.isHome;
2677
+ const local = isHome ? void 0 : folderPaths(folder);
2678
+ const state = workspacePaths(id);
2679
+ const layered = local && local.dir !== paths.home ? local : void 0;
2680
+ this.settings = new LayeredSettings(shared.rootSettings, layered?.settings);
2681
+ this.layoutFile = new JsonFile(state.layout);
2682
+ this.view = new WebContentsView({ webPreferences: {
2683
+ preload: shared.preloadPath,
2684
+ contextIsolation: true,
2685
+ nodeIntegration: false,
2686
+ sandbox: true
2687
+ } });
2688
+ this.view.setBackgroundColor("#0a0a0a");
2689
+ this.view.webContents.setWindowOpenHandler(({ url }) => {
2690
+ shell.openExternal(url);
2691
+ return { action: "deny" };
2692
+ });
2693
+ this.server.connect(createElectronTransport(this.view.webContents));
2694
+ const rootSettings = shared.rootSettings.get();
2695
+ const agentCwd = isHome ? rootSettings.agent.cwd ?? homedir() : folder;
2696
+ this.agent = new AgentHost({
2697
+ cwd: agentCwd,
2698
+ createHarness: (emit) => new PiHarness({
2699
+ agentDir: paths.agentDir,
2700
+ cwd: agentCwd,
2701
+ model: rootSettings.agent.model,
2702
+ skillPaths: [join(shared.resourcesDir, "skills")],
2703
+ appendSystemPrompt: nubaseSystemPrompt({
2704
+ folder,
2705
+ isHome,
2706
+ nubaseHome: paths.home,
2707
+ globalExtensionsDir: paths.extensions,
2708
+ folderNubaseDir: layered?.dir
2709
+ }),
2710
+ dialogs: this.agentDialogs(),
2711
+ emit
2712
+ }),
2713
+ onEvent: (event) => this.server.emit("agent.event", event)
2714
+ });
2715
+ if (layered && existsSync(layered.extensions)) this.folderExtensions = this.createFolderSource(layered.extensions);
2716
+ this.extensions = new ExtensionHost({
2717
+ workspace: this.info,
2718
+ sources: [this.folderExtensions, shared.globalExtensions].filter((s) => Boolean(s)),
2719
+ agent: this.agent,
2720
+ fs: shared.fs,
2721
+ secrets: shared.secrets,
2722
+ storage: shared.storage,
2723
+ safeMode: shared.safeMode,
2724
+ builtins: BUILTIN_EXTENSIONS.map((b) => ({
2725
+ ...b,
2726
+ host: b.id === "terminal" ? createTerminalHost({
2727
+ PI_CODING_AGENT_DIR: paths.agentDir,
2728
+ NUBASE_EXTENSIONS_DIR: paths.extensions,
2729
+ NUBASE_FOLDER: folder
2730
+ }) : b.id === "files" ? createFilesHost() : void 0
2731
+ })),
2732
+ isDisabled: (extensionId) => this.settings.get().disabledExtensions.includes(extensionId),
2733
+ promptSecret: (title) => this.server.requestRenderer("dialogs.input", title, { secret: true }),
2734
+ onChanged: (list) => this.server.emit("extensions.changed", list),
2735
+ onEvent: (extensionId, event, payload) => this.server.emit("extensions.event", {
2736
+ extensionId,
2737
+ event,
2738
+ payload
2739
+ })
2740
+ });
2741
+ for (const tool of createNubaseTools({
2742
+ server: this.server,
2743
+ extensions: this.extensions,
2744
+ globalExtensionsDir: paths.extensions,
2745
+ folderExtensionsDir: layered?.extensions,
2746
+ ensureFolderExtensions: async () => {
2747
+ if (!layered) throw new Error("The ~ tab has no folder extensions");
2748
+ await this.ensureFolderExtensions(layered.extensions);
2749
+ return layered.extensions;
2750
+ },
2751
+ templateDir: join(shared.resourcesDir, "templates", "extension"),
2752
+ getWebContents: () => this.view.webContents.isDestroyed() ? void 0 : this.view.webContents
2753
+ })) this.agent.registerTool("nubase", tool);
2754
+ this.registerHandlers(layered?.extensions);
2755
+ }
2756
+ get info() {
2757
+ return this.tabs.info(this.id);
2758
+ }
2759
+ /** The folder's own extensions, if it has any; asset URLs use the workspace id as their key. */
2760
+ get folderSource() {
2761
+ return this.folderExtensions;
2762
+ }
2763
+ async start() {
2764
+ if (this.info.isHome) await migrateLegacyLayout(this.layoutFile);
2765
+ await this.settings.load();
2766
+ this.disposers.push(this.settings.onChange((value) => {
2767
+ this.server.emit("settings.changed", value);
2768
+ this.extensions.refresh();
2769
+ }), this.layoutFile.onExternalChange((layout) => this.server.emit("layout.changedOnDisk", layout)));
2770
+ const { rendererUrl } = this.shared;
2771
+ if ("url" in rendererUrl) this.view.webContents.loadURL(rendererUrl.url);
2772
+ else this.view.webContents.loadFile(rendererUrl.file);
2773
+ this.agent.start();
2774
+ this.extensions.start().catch((error) => console.error(`Extensions failed to start for ${this.folder}`, error));
2775
+ }
2776
+ dispose() {
2777
+ for (const dispose of this.disposers.splice(0)) dispose();
2778
+ for (const watch of this.fsWatches.values()) watch.dispose();
2779
+ this.fsWatches.clear();
2780
+ this.extensions.dispose();
2781
+ this.agent.dispose();
2782
+ this.folderExtensions?.dispose();
2783
+ this.settings.dispose();
2784
+ if (!this.view.webContents.isDestroyed()) this.view.webContents.close();
2785
+ }
2786
+ createFolderSource(dir) {
2787
+ return new ExtensionSource({
2788
+ key: this.id,
2789
+ kind: "folder",
2790
+ dir,
2791
+ builder: this.shared.builder,
2792
+ repo: new ExtensionsRepo(dir, workspacePaths(this.id).extensionsGitDir)
2793
+ });
2794
+ }
2795
+ async ensureFolderExtensions(dir) {
2796
+ if (this.folderExtensions) return;
2797
+ await prepareExtensionsWorkspace(dir, this.shared.resourcesDir, { inProject: true });
2798
+ this.folderExtensions = this.createFolderSource(dir);
2799
+ await this.extensions.addSource(this.folderExtensions);
2800
+ }
2801
+ agentDialogs() {
2802
+ const server = this.server;
2803
+ return {
2804
+ confirm: (title, message) => server.requestRenderer("dialogs.confirm", title, message),
2805
+ input: (title, options) => {
2806
+ const { signal, ...inputOptions } = options ?? {};
2807
+ if (signal?.aborted) return Promise.resolve(void 0);
2808
+ const dialogId = randomUUID();
2809
+ const cancel = () => {
2810
+ server.requestRenderer("dialogs.cancelInput", dialogId).catch(() => {});
2811
+ };
2812
+ signal?.addEventListener("abort", cancel, { once: true });
2813
+ return server.requestRenderer("dialogs.input", title, {
2814
+ ...inputOptions,
2815
+ dialogId
2816
+ }).finally(() => signal?.removeEventListener("abort", cancel));
2817
+ },
2818
+ select: (title, options) => server.requestRenderer("dialogs.select", title, options),
2819
+ notify: (message, level) => void server.requestRenderer("notify", message, level).catch(() => {}),
2820
+ openExternal: (url) => {
2821
+ if (/^https?:\/\//.test(url)) shell.openExternal(url);
2822
+ }
2823
+ };
2824
+ }
2825
+ registerHandlers(folderExtensionsDir) {
2826
+ const { fs, storage, safeMode } = this.shared;
2827
+ const { server, settings, agent, extensions } = this;
2828
+ server.handle({
2829
+ "app.info": () => ({
2830
+ version: this.shared.appVersion(),
2831
+ platform: process.platform,
2832
+ homeDir: fs.homeDir,
2833
+ nubaseHome: paths.home,
2834
+ extensionsDir: paths.extensions,
2835
+ folderExtensionsDir,
2836
+ safeMode,
2837
+ workspace: this.info
2838
+ }),
2839
+ "app.openExternal": (url) => {
2840
+ if (/^https?:\/\//.test(url)) shell.openExternal(url);
2841
+ },
2842
+ "app.reportLog": (entry) => extensions.reportLog(entry),
2843
+ "fs.readText": (path) => fs.readText(path),
2844
+ "fs.writeText": (path, content) => fs.writeText(path, content),
2845
+ "fs.stat": (path) => fs.stat(path),
2846
+ "fs.readDirectory": (path) => fs.readDirectory(path),
2847
+ "fs.mkdir": (path) => fs.mkdir(path),
2848
+ "fs.delete": (path, recursive) => fs.delete(path, { recursive }),
2849
+ "fs.rename": (from, to) => fs.rename(from, to),
2850
+ "fs.watch": (path, recursive) => {
2851
+ const watchId = `w${this.watchCounter++}`;
2852
+ this.fsWatches.set(watchId, fs.watch(path, (change) => server.emit("fs.change", {
2853
+ watchId,
2854
+ change
2855
+ }), { recursive }));
2856
+ return watchId;
2857
+ },
2858
+ "fs.unwatch": (watchId) => {
2859
+ this.fsWatches.get(watchId)?.dispose();
2860
+ this.fsWatches.delete(watchId);
2861
+ },
2862
+ "fs.thumbnail": async (path, size) => {
2863
+ if (process.platform !== "darwin" && process.platform !== "win32") return;
2864
+ try {
2865
+ const image = await nativeImage.createThumbnailFromPath(path, {
2866
+ width: size,
2867
+ height: size
2868
+ });
2869
+ return image.isEmpty() ? void 0 : image.toDataURL();
2870
+ } catch {
2871
+ return;
2872
+ }
2873
+ },
2874
+ "shell.openPath": async (path) => {
2875
+ const error = await shell.openPath(path);
2876
+ if (error) throw new Error(error);
2877
+ },
2878
+ "shell.showItemInFolder": (path) => shell.showItemInFolder(path),
2879
+ "shell.quickLook": (path) => {
2880
+ if (process.platform !== "darwin") return false;
2881
+ const window = BaseWindow.getAllWindows().find((w) => w.contentView.children.includes(this.view));
2882
+ if (!window) return false;
2883
+ window.previewFile(path);
2884
+ return true;
2885
+ },
2886
+ "settings.get": () => settings.get(),
2887
+ "settings.update": (patch, scope) => settings.update(patch, scope),
2888
+ "settings.getFolderOverrides": () => settings.getFolderOverrides(),
2889
+ "layout.load": () => this.layoutFile.read(),
2890
+ "layout.save": (layout) => this.layoutFile.write(layout),
2891
+ "storage.get": (scope, key) => storage.get(scope, key),
2892
+ "storage.set": (scope, key, value) => storage.set(scope, key, value),
2893
+ "workspaces.list": () => this.tabs.list(),
2894
+ "workspaces.open": (folder) => this.tabs.open(folder),
2895
+ "workspaces.close": (id) => this.tabs.close(id),
2896
+ "workspaces.activate": (id) => this.tabs.activate(id),
2897
+ "agent.getState": () => agent.getState(),
2898
+ "agent.getMessages": () => agent.getMessages(),
2899
+ "agent.prompt": (text, images) => agent.prompt(text, images),
2900
+ "agent.steer": (text, images) => agent.steer(text, images),
2901
+ "agent.abort": () => agent.abort(),
2902
+ "agent.newSession": () => agent.newSession(),
2903
+ "agent.listSessions": () => agent.listSessions(),
2904
+ "agent.switchSession": (path) => agent.switchSession(path),
2905
+ "agent.listModels": () => agent.listModels(),
2906
+ "agent.setModel": async (provider, id) => {
2907
+ await agent.setModel(provider, id);
2908
+ await settings.update({ agent: {
2909
+ ...this.shared.rootSettings.get().agent,
2910
+ model: {
2911
+ provider,
2912
+ id
2913
+ }
2914
+ } });
2915
+ },
2916
+ "agent.listProviders": () => agent.listProviders(),
2917
+ "agent.login": (providerId, method) => agent.login(providerId, method),
2918
+ "agent.logout": (providerId) => agent.logout(providerId),
2919
+ "extensions.list": () => extensions.list(),
2920
+ "extensions.rebuild": (id) => extensions.rebuild(id),
2921
+ "extensions.setEnabled": async (id, enabled) => {
2922
+ const disabled = new Set(settings.get().disabledExtensions);
2923
+ if (enabled) disabled.delete(id);
2924
+ else disabled.add(id);
2925
+ const scope = settings.getFolderOverrides().disabledExtensions !== void 0 ? "folder" : "root";
2926
+ await settings.update({ disabledExtensions: [...disabled] }, scope);
2927
+ await extensions.setEnabled(id);
2928
+ },
2929
+ "extensions.revert": (id) => extensions.revert(id),
2930
+ "extensions.reveal": (id) => {
2931
+ shell.showItemInFolder(join(extensions.pathOf(id), "package.json"));
2932
+ },
2933
+ "extensions.call": (extensionId, method, args) => extensions.call(extensionId, method, args)
2934
+ });
2935
+ }
2936
+ };
2937
+ //#endregion
2938
+ //#region src/main/services/workspaces/WorkspaceManager.ts
2939
+ var isDirectory = (path) => {
2940
+ try {
2941
+ return statSync(path).isDirectory();
2942
+ } catch {
2943
+ return false;
2944
+ }
2945
+ };
2946
+ /**
2947
+ * The tab bar: which folders are open, which one is showing, and the
2948
+ * folder tabs (each a Workspace with its own renderer view) behind them.
2949
+ * `~` is always the first tab and cannot be closed.
2950
+ */
2951
+ var WorkspaceManager = class {
2952
+ folders = [];
2953
+ activeId = HOME_WORKSPACE_ID;
2954
+ workspaces = /* @__PURE__ */ new Map();
2955
+ starting = /* @__PURE__ */ new Map();
2956
+ window;
2957
+ file = new JsonFile(paths.workspaces);
2958
+ constructor(shared) {
2959
+ this.shared = shared;
2960
+ }
2961
+ async restore() {
2962
+ const stored = await this.file.read();
2963
+ const home = homedir();
2964
+ const seen = /* @__PURE__ */ new Set();
2965
+ this.folders = (stored?.folders ?? []).map((folder) => resolve(folder)).filter((folder) => {
2966
+ if (folder === home || seen.has(folder) || !isDirectory(folder)) return false;
2967
+ seen.add(folder);
2968
+ return true;
2969
+ });
2970
+ const activeId = stored?.activeId;
2971
+ this.activeId = activeId && this.folderOf(activeId) ? activeId : HOME_WORKSPACE_ID;
2972
+ }
2973
+ /** Shows the folder tabs in a (new) window, starting with the active one. */
2974
+ async attach(window) {
2975
+ this.window = window;
2976
+ window.on("resize", () => this.layoutViews());
2977
+ window.on("closed", () => {
2978
+ if (this.window === window) this.window = void 0;
2979
+ for (const workspace of this.workspaces.values()) workspace.dispose();
2980
+ this.workspaces.clear();
2981
+ });
2982
+ await this.activate(this.activeId);
2983
+ }
2984
+ list() {
2985
+ return {
2986
+ workspaces: this.ids().map((id) => this.info(id)),
2987
+ activeId: this.activeId
2988
+ };
2989
+ }
2990
+ info(id) {
2991
+ const folder = this.folderOf(id);
2992
+ if (!folder) throw new Error(`No open folder "${id}"`);
2993
+ if (id === "home") return {
2994
+ id,
2995
+ folder,
2996
+ title: "~",
2997
+ isHome: true
2998
+ };
2999
+ const name = basename(folder) || folder;
3000
+ return {
3001
+ id,
3002
+ folder,
3003
+ title: this.folders.some((other) => other !== folder && basename(other) === name) ? `${basename(dirname(folder))}/${name}` : name,
3004
+ isHome: false
3005
+ };
3006
+ }
3007
+ async open(folder) {
3008
+ let target = folder;
3009
+ if (!target) {
3010
+ const options = {
3011
+ title: "Open Folder",
3012
+ properties: ["openDirectory", "createDirectory"]
3013
+ };
3014
+ const result = this.window ? await dialog.showOpenDialog(this.window, options) : await dialog.showOpenDialog(options);
3015
+ target = result.canceled ? void 0 : result.filePaths[0];
3016
+ if (!target) return void 0;
3017
+ }
3018
+ target = resolve(target);
3019
+ if (!isDirectory(target)) throw new Error(`Not a folder: ${target}`);
3020
+ const id = workspaceIdOf(target);
3021
+ if (id !== "home" && !this.folders.includes(target)) this.folders.push(target);
3022
+ await this.activate(id);
3023
+ return this.info(id);
3024
+ }
3025
+ async close(id) {
3026
+ if (id === "home") throw new Error("The ~ tab cannot be closed");
3027
+ const ids = this.ids();
3028
+ const index = ids.indexOf(id);
3029
+ if (index < 0) return;
3030
+ const folder = this.folderOf(id);
3031
+ this.folders = this.folders.filter((f) => f !== folder);
3032
+ const workspace = this.workspaces.get(id);
3033
+ this.workspaces.delete(id);
3034
+ if (workspace) {
3035
+ this.window?.contentView.removeChildView(workspace.view);
3036
+ workspace.dispose();
3037
+ }
3038
+ if (this.activeId === id) await this.activate(ids[index - 1] ?? "home");
3039
+ else {
3040
+ await this.persist();
3041
+ this.broadcast();
3042
+ }
3043
+ }
3044
+ async activate(id) {
3045
+ if (!this.folderOf(id)) throw new Error(`No open folder "${id}"`);
3046
+ this.activeId = id;
3047
+ await this.persist();
3048
+ this.broadcast();
3049
+ const window = this.window;
3050
+ if (!window) return;
3051
+ const workspace = await this.ensure(id);
3052
+ if (this.activeId !== id || window.isDestroyed()) return;
3053
+ for (const other of this.workspaces.values()) other.view.setVisible(other === workspace);
3054
+ this.layoutViews();
3055
+ workspace.view.webContents.focus();
3056
+ }
3057
+ /** Activates the tab `offset` places away from the current one, wrapping around. */
3058
+ async activateRelative(offset) {
3059
+ const ids = this.ids();
3060
+ const next = ids[(ids.indexOf(this.activeId) + offset + ids.length) % ids.length];
3061
+ if (next) await this.activate(next);
3062
+ }
3063
+ /** Resolves `nubase-ext://<key>/<id>/<file>`: `global`, or a workspace id for that folder's extensions. */
3064
+ resolveAsset(url) {
3065
+ const parsed = new URL(url);
3066
+ if (parsed.protocol !== `nubase-ext:`) return void 0;
3067
+ return (parsed.hostname === this.shared.globalExtensions.key ? this.shared.globalExtensions : this.workspaces.get(parsed.hostname)?.folderSource)?.resolveAsset(parsed.pathname);
3068
+ }
3069
+ dispose() {
3070
+ for (const workspace of this.workspaces.values()) workspace.dispose();
3071
+ this.workspaces.clear();
3072
+ }
3073
+ ids() {
3074
+ return [HOME_WORKSPACE_ID, ...this.folders.map(workspaceIdOf)];
3075
+ }
3076
+ folderOf(id) {
3077
+ if (id === "home") return homedir();
3078
+ return this.folders.find((folder) => workspaceIdOf(folder) === id);
3079
+ }
3080
+ ensure(id) {
3081
+ const existing = this.workspaces.get(id);
3082
+ if (existing) return Promise.resolve(existing);
3083
+ let pending = this.starting.get(id);
3084
+ if (!pending) {
3085
+ pending = (async () => {
3086
+ const folder = this.folderOf(id);
3087
+ if (!folder) throw new Error(`No open folder "${id}"`);
3088
+ const workspace = new Workspace(id, folder, this.shared, {
3089
+ info: (wid) => this.info(wid),
3090
+ list: () => this.list(),
3091
+ open: (f) => this.open(f),
3092
+ close: (wid) => this.close(wid),
3093
+ activate: (wid) => this.activate(wid)
3094
+ });
3095
+ workspace.view.setVisible(false);
3096
+ this.window?.contentView.addChildView(workspace.view);
3097
+ this.workspaces.set(id, workspace);
3098
+ this.layoutViews();
3099
+ await workspace.start();
3100
+ return workspace;
3101
+ })().finally(() => this.starting.delete(id));
3102
+ this.starting.set(id, pending);
3103
+ }
3104
+ return pending;
3105
+ }
3106
+ layoutViews() {
3107
+ if (!this.window || this.window.isDestroyed()) return;
3108
+ const { width, height } = this.window.getContentBounds();
3109
+ for (const workspace of this.workspaces.values()) workspace.view.setBounds({
3110
+ x: 0,
3111
+ y: 0,
3112
+ width,
3113
+ height
3114
+ });
3115
+ }
3116
+ broadcast() {
3117
+ const list = this.list();
3118
+ for (const workspace of this.workspaces.values()) workspace.server.emit("workspaces.changed", list);
3119
+ }
3120
+ persist() {
3121
+ return this.file.write({
3122
+ folders: this.folders,
3123
+ activeId: this.activeId
3124
+ });
3125
+ }
3126
+ };
3127
+ //#endregion
3128
+ //#region src/main/index.ts
3129
+ var safeMode = process.argv.includes("--safe-mode");
3130
+ protocol.registerSchemesAsPrivileged([{
3131
+ scheme: EXTENSION_PROTOCOL,
3132
+ privileges: {
3133
+ standard: true,
3134
+ secure: true,
3135
+ supportFetchAPI: true,
3136
+ corsEnabled: true,
3137
+ stream: true
3138
+ }
3139
+ }, {
3140
+ scheme: FILE_PROTOCOL,
3141
+ privileges: {
3142
+ standard: true,
3143
+ secure: true,
3144
+ supportFetchAPI: true,
3145
+ corsEnabled: true,
3146
+ stream: true
3147
+ }
3148
+ }]);
3149
+ var MIME_TYPES = {
3150
+ js: "text/javascript",
3151
+ css: "text/css",
3152
+ json: "application/json",
3153
+ map: "application/json",
3154
+ png: "image/png",
3155
+ svg: "image/svg+xml"
3156
+ };
3157
+ /** The window is only a frame: each folder tab is a view inside it with its own renderer. */
3158
+ var createWindow = () => {
3159
+ return new BrowserWindow({
3160
+ width: 1440,
3161
+ height: 900,
3162
+ minWidth: 720,
3163
+ minHeight: 480,
3164
+ show: false,
3165
+ backgroundColor: "#0a0a0a",
3166
+ titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
3167
+ trafficLightPosition: {
3168
+ x: 14,
3169
+ y: 13
3170
+ }
3171
+ });
3172
+ };
3173
+ var main = async () => {
3174
+ await app.whenReady();
3175
+ const resourcesDir = join(app.getAppPath(), "resources");
3176
+ const rootSettings = new SettingsService(paths.settings);
3177
+ await rootSettings.load();
3178
+ await prepareExtensionsWorkspace(paths.extensions, resourcesDir);
3179
+ const builder = new ExtensionBuilder();
3180
+ const workspaces = new WorkspaceManager({
3181
+ appVersion: () => app.getVersion(),
3182
+ resourcesDir,
3183
+ preloadPath: join(import.meta.dirname, "../preload/index.cjs"),
3184
+ rendererUrl: process.env.ELECTRON_RENDERER_URL ? { url: process.env.ELECTRON_RENDERER_URL } : { file: join(import.meta.dirname, "../renderer/index.html") },
3185
+ safeMode,
3186
+ fs: new LocalFileSystem(),
3187
+ storage: new StorageService(paths.storage),
3188
+ secrets: new SecretStore(paths.secrets),
3189
+ rootSettings,
3190
+ builder,
3191
+ globalExtensions: new ExtensionSource({
3192
+ key: "global",
3193
+ kind: "global",
3194
+ dir: paths.extensions,
3195
+ builder,
3196
+ repo: new ExtensionsRepo(paths.extensions)
3197
+ })
3198
+ });
3199
+ await workspaces.restore();
3200
+ installApplicationMenu({ openFolder: () => void workspaces.open().catch(console.error) });
3201
+ protocol.handle(EXTENSION_PROTOCOL, async (request) => {
3202
+ const file = workspaces.resolveAsset(request.url);
3203
+ if (!file) return new Response("Not found", { status: 404 });
3204
+ const response = await net.fetch(pathToFileURL(file).toString());
3205
+ const extension = file.split(".").pop() ?? "";
3206
+ return new Response(response.body, {
3207
+ status: response.status,
3208
+ headers: {
3209
+ "Content-Type": MIME_TYPES[extension] ?? "application/octet-stream",
3210
+ "Access-Control-Allow-Origin": "*",
3211
+ "Cache-Control": "no-store"
3212
+ }
3213
+ });
3214
+ });
3215
+ protocol.handle(FILE_PROTOCOL, handleFileRequest);
3216
+ let mainWindow;
3217
+ const openWindow = async () => {
3218
+ const window = createWindow();
3219
+ mainWindow = window;
3220
+ window.on("closed", () => {
3221
+ if (mainWindow === window) mainWindow = void 0;
3222
+ });
3223
+ await workspaces.attach(window);
3224
+ window.show();
3225
+ };
3226
+ await openWindow();
3227
+ app.on("before-quit", () => workspaces.dispose());
3228
+ app.on("activate", () => {
3229
+ if (!mainWindow) openWindow();
3230
+ });
3231
+ };
3232
+ app.on("window-all-closed", () => {
3233
+ if (process.platform !== "darwin") app.quit();
3234
+ });
3235
+ main().catch((error) => {
3236
+ console.error("Nubase failed to start", error);
3237
+ app.quit();
3238
+ });
3239
+ //#endregion
3240
+ export {};