@andespindola/brainlink 0.1.0-beta.33 → 0.1.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -24,6 +24,9 @@
24
24
  - Added `docs/QUICKSTART.md` and aligned README/agent docs with the latest CLI/MCP flows.
25
25
  - Added middle-out context assembly so chunk selection expands around the strongest note chunk.
26
26
  - Added compressed-space pack prefiltering (token bloom index) before `.blpk` decryption and scan.
27
+ - Improved graph UI auto-fit and viewport recovery so loaded nodes are re-centered when zoom/pan drifts to empty canvas.
28
+ - Added cross-platform native desktop GUI auto-open for `blink server` (macOS Swift/WebKit, Windows PowerShell WinForms, Linux Python GTK/WebKit2), with app-window/browser fallback.
29
+ - Changed Linux default UI launch to app-window/browser for lighter startup; Linux native GUI is now opt-in via `BRAINLINK_LINUX_NATIVE_GUI=1`.
27
30
 
28
31
  ## 0.1.0-beta.3
29
32
 
package/README.md CHANGED
@@ -553,8 +553,14 @@ blink server --host 127.0.0.1 --port 4321 --watch
553
553
  ```
554
554
 
555
555
  By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` only when you want to inspect a custom vault.
556
- By default, `blink server` tries to open the graph in a dedicated app window (`--app=<url>` on compatible browsers).
557
- If app-window launch is unavailable, it falls back to the default browser. Use `--no-open` to keep it headless.
556
+ By default, `blink server` tries to open the graph in a native desktop GUI window:
557
+ - macOS: Swift + WebKit
558
+ - Windows: PowerShell WinForms WebBrowser
559
+ - Linux: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
560
+
561
+ On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
562
+ If native GUI launch is unavailable on your system, it falls back to dedicated app-window mode and then to the default browser.
563
+ Use `--no-open` to keep it headless.
558
564
 
559
565
  The graph UI shows:
560
566
 
@@ -844,7 +850,9 @@ blink server --vault ./vault --watch --no-open
844
850
  ```
845
851
 
846
852
  Starts the local read-only graph UI and HTTP API.
847
- By default, it tries to open a dedicated app window for the graph URL and falls back to browser open when app-window mode is unavailable.
853
+ By default, it tries to open a native desktop GUI window for the graph URL.
854
+ On Linux, native GUI is disabled by default; enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
855
+ If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
848
856
  Use `--no-open` to skip that behavior.
849
857
 
850
858
  The HTTP server only binds to loopback hosts such as `127.0.0.1`, `localhost` or `::1`.
@@ -245,8 +245,8 @@ li small {
245
245
  }
246
246
 
247
247
  .content-dialog {
248
- width: min(920px, calc(100vw - 32px));
249
- max-height: calc(100svh - 32px);
248
+ width: min(1240px, calc(100vw - 24px));
249
+ max-height: calc(100svh - 20px);
250
250
  padding: 0;
251
251
  border: 1px solid var(--line);
252
252
  border-radius: 8px;
@@ -263,7 +263,7 @@ li small {
263
263
  .content-dialog article {
264
264
  display: grid;
265
265
  grid-template-rows: auto auto minmax(0, 1fr);
266
- max-height: calc(100svh - 34px);
266
+ max-height: calc(100svh - 22px);
267
267
  }
268
268
 
269
269
  .content-dialog header {
@@ -339,7 +339,7 @@ li small {
339
339
 
340
340
  .content-meta-section ul,
341
341
  .content-meta-section .tags {
342
- max-height: 140px;
342
+ max-height: 220px;
343
343
  overflow: auto;
344
344
  align-content: flex-start;
345
345
  padding-right: 4px;
@@ -5,6 +5,8 @@ const largeGraphEdgeRenderLimit = 16000
5
5
  const renderNodeBudget = 1800
6
6
  const minNodePixelRadius = 1.8
7
7
  const viewportPaddingPx = 280
8
+ const worldCoordinateLimit = 5_000_000
9
+ const transformCoordinateLimit = 20_000_000
8
10
  const state = {
9
11
  graph: { nodes: [], edges: [] },
10
12
  nodes: [],
@@ -26,7 +28,9 @@ const state = {
26
28
  cursor: { x: 0, y: 0, inCanvas: false },
27
29
  graphSignature: '',
28
30
  graphStatus: '',
29
- last: performance.now()
31
+ last: performance.now(),
32
+ offscreenFrameCount: 0,
33
+ recoveringViewport: false
30
34
  }
31
35
 
32
36
  const byId = id => document.getElementById(id)
@@ -130,6 +134,8 @@ const recomputeVisibility = () => {
130
134
  const edgeWeight = edge => Number.isFinite(edge.weight) ? Math.max(1, edge.weight) : 1
131
135
 
132
136
  const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
137
+ const isFiniteNumber = value => Number.isFinite(value)
138
+ const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
133
139
 
134
140
  const graphBounds = nodes => {
135
141
  if (nodes.length === 0) return null
@@ -157,14 +163,25 @@ const graphBounds = nodes => {
157
163
  }
158
164
 
159
165
  const fitScaleBiasByNodeCount = nodeCount => {
160
- if (nodeCount <= 6) return 2.8
161
- if (nodeCount <= 20) return 2.2
162
- if (nodeCount <= 60) return 1.72
163
- if (nodeCount <= 180) return 1.34
164
- if (nodeCount <= 600) return 1.08
165
- if (nodeCount <= 2000) return 0.9
166
- if (nodeCount <= 6000) return 0.72
167
- return 0.58
166
+ if (nodeCount <= 6) return 1.22
167
+ if (nodeCount <= 20) return 1.12
168
+ if (nodeCount <= 60) return 1.04
169
+ if (nodeCount <= 180) return 1
170
+ if (nodeCount <= 600) return 0.94
171
+ if (nodeCount <= 2000) return 0.82
172
+ if (nodeCount <= 6000) return 0.68
173
+ return 0.56
174
+ }
175
+
176
+ const autoFitScaleRangeByNodeCount = nodeCount => {
177
+ if (nodeCount <= 6) return { min: 0.4, max: 2.2 }
178
+ if (nodeCount <= 20) return { min: 0.34, max: 1.65 }
179
+ if (nodeCount <= 60) return { min: 0.25, max: 1.22 }
180
+ if (nodeCount <= 180) return { min: 0.18, max: 0.92 }
181
+ if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
182
+ if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
183
+ if (nodeCount <= 6000) return { min: 0.06, max: 0.32 }
184
+ return { min: zoomRange.min, max: 0.24 }
168
185
  }
169
186
 
170
187
  const fitView = (options = { useFiltered: true }) => {
@@ -188,25 +205,13 @@ const fitView = (options = { useFiltered: true }) => {
188
205
  if (nodeCount <= 2000) return 140
189
206
  return 180
190
207
  }
191
- const minFitScaleByNodeCount = nodeCount => {
192
- if (nodeCount <= 6) return 2.4
193
- if (nodeCount <= 20) return 1.8
194
- if (nodeCount <= 60) return 1.2
195
- if (nodeCount <= 180) return 0.86
196
- if (nodeCount <= 600) return 0.58
197
- if (nodeCount <= 2000) return 0.34
198
- if (nodeCount <= 6000) return 0.2
199
- return 0.13
200
- }
201
-
202
208
  const padding = paddingByNodeCount(nodes.length)
203
209
  const scaleX = width / (bounds.width + padding * 2)
204
210
  const scaleY = height / (bounds.height + padding * 2)
205
- const fitScale = clampScale(Math.min(scaleX, scaleY))
211
+ const fitScale = Math.min(scaleX, scaleY)
206
212
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
207
- const minimumScale = minFitScaleByNodeCount(nodes.length)
208
- const minimumLargeGraphScale = nodes.length > largeGraphNodeThreshold ? 0.13 : zoomRange.min
209
- const scale = Math.max(biasedScale, minimumScale, minimumLargeGraphScale)
213
+ const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
214
+ const scale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
210
215
  const centerX = (bounds.minX + bounds.maxX) / 2
211
216
  const centerY = (bounds.minY + bounds.maxY) / 2
212
217
 
@@ -328,8 +333,8 @@ const tick = delta => {
328
333
  const dy = target.y - source.y
329
334
  const distance = Math.max(Math.hypot(dx, dy), 1)
330
335
  const force = (distance - 150) * 0.002 * strength
331
- const fx = dx * force
332
- const fy = dy * force
336
+ const fx = (dx / distance) * force
337
+ const fy = (dy / distance) * force
333
338
  source.vx += fx
334
339
  source.vy += fy
335
340
  target.vx -= fx
@@ -502,6 +507,40 @@ const computeRenderVisibility = () => {
502
507
  state.renderEdges = edges
503
508
  }
504
509
 
510
+ const isNodeVisibleOnScreen = (node, width, height) => {
511
+ const radius = nodeRadius(node) * state.transform.scale
512
+ const screenX = node.x * state.transform.scale + state.transform.x
513
+ const screenY = node.y * state.transform.scale + state.transform.y
514
+
515
+ return (
516
+ screenX + radius >= 0 &&
517
+ screenX - radius <= width &&
518
+ screenY + radius >= 0 &&
519
+ screenY - radius <= height
520
+ )
521
+ }
522
+
523
+ const hasValidTransform = () =>
524
+ isFiniteNumber(state.transform.x) &&
525
+ isFiniteNumber(state.transform.y) &&
526
+ isFiniteNumber(state.transform.scale) &&
527
+ Math.abs(state.transform.x) <= transformCoordinateLimit &&
528
+ Math.abs(state.transform.y) <= transformCoordinateLimit &&
529
+ state.transform.scale > 0
530
+
531
+ const sanitizeNodePosition = node => {
532
+ if (!isReasonableCoordinate(node.x)) node.x = 0
533
+ if (!isReasonableCoordinate(node.y)) node.y = 0
534
+ if (!isFiniteNumber(node.vx) || Math.abs(node.vx) > worldCoordinateLimit) node.vx = 0
535
+ if (!isFiniteNumber(node.vy) || Math.abs(node.vy) > worldCoordinateLimit) node.vy = 0
536
+ }
537
+
538
+ const sanitizeGraphState = () => {
539
+ state.nodes.forEach(sanitizeNodePosition)
540
+ state.visibleNodes.forEach(sanitizeNodePosition)
541
+ state.renderNodes.forEach(sanitizeNodePosition)
542
+ }
543
+
505
544
  const render = now => {
506
545
  const delta = now - state.last
507
546
  state.last = now
@@ -513,6 +552,10 @@ const render = now => {
513
552
  const rect = canvas.getBoundingClientRect()
514
553
  const width = Math.max(rect.width, 320)
515
554
  const height = Math.max(rect.height, 320)
555
+ sanitizeGraphState()
556
+ if (!hasValidTransform()) {
557
+ resetView()
558
+ }
516
559
  ctx.clearRect(0, 0, width, height)
517
560
  if (state.nodes.length === 0) {
518
561
  ctx.fillStyle = '#99a5b5'
@@ -528,6 +571,20 @@ const render = now => {
528
571
 
529
572
  computeRenderVisibility()
530
573
  tick(delta)
574
+ const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
575
+ if (!hasVisibleNodeOnScreen && state.renderNodes.length > 0) {
576
+ state.offscreenFrameCount += 1
577
+ if (state.offscreenFrameCount >= 6 && !state.recoveringViewport) {
578
+ state.recoveringViewport = true
579
+ fitView({ useFiltered: true })
580
+ state.offscreenFrameCount = 0
581
+ requestAnimationFrame(() => {
582
+ state.recoveringViewport = false
583
+ })
584
+ }
585
+ } else {
586
+ state.offscreenFrameCount = 0
587
+ }
531
588
  const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
532
589
  if (drawEdges) {
533
590
  state.renderEdges.forEach(edge => {
@@ -695,6 +752,10 @@ const isScreenPointInsideCanvas = (screenX, screenY) => {
695
752
  }
696
753
 
697
754
  const handleWheelZoom = event => {
755
+ if (elements.contentDialog?.open) {
756
+ return
757
+ }
758
+
698
759
  if (!isScreenPointInsideCanvas(event.clientX, event.clientY)) {
699
760
  return
700
761
  }
@@ -1,9 +1,8 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { existsSync } from 'node:fs';
4
- import { dirname, relative, resolve } from 'node:path';
5
- import { platform } from 'node:os';
6
- import { spawn } from 'node:child_process';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { platform, tmpdir } from 'node:os';
5
+ import { spawn, spawnSync } from 'node:child_process';
7
6
  import { addNote } from '../../application/add-note.js';
8
7
  import { buildContextPackage } from '../../application/build-context.js';
9
8
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
@@ -37,6 +36,200 @@ const spawnDetached = (command, args) => {
37
36
  return false;
38
37
  }
39
38
  };
39
+ const nativeGuiSwiftScriptPath = join(tmpdir(), 'brainlink-native-gui.swift');
40
+ const nativeGuiPowershellScriptPath = join(tmpdir(), 'brainlink-native-gui.ps1');
41
+ const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py');
42
+ const nativeGuiSwiftScript = `import Foundation
43
+ import AppKit
44
+ import WebKit
45
+
46
+ final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
47
+ private let targetUrl: URL
48
+ private var window: NSWindow?
49
+ private var webView: WKWebView?
50
+
51
+ init(targetUrl: URL) {
52
+ self.targetUrl = targetUrl
53
+ }
54
+
55
+ func applicationDidFinishLaunching(_ notification: Notification) {
56
+ let window = NSWindow(
57
+ contentRect: NSRect(x: 0, y: 0, width: 1320, height: 860),
58
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
59
+ backing: .buffered,
60
+ defer: false
61
+ )
62
+ window.title = "Brainlink Graph"
63
+ window.center()
64
+ window.isReleasedWhenClosed = false
65
+ window.delegate = self
66
+
67
+ let webView = WKWebView(frame: window.contentView?.bounds ?? .zero)
68
+ webView.autoresizingMask = [.width, .height]
69
+ webView.allowsBackForwardNavigationGestures = true
70
+ webView.load(URLRequest(url: targetUrl))
71
+ window.contentView?.addSubview(webView)
72
+
73
+ self.window = window
74
+ self.webView = webView
75
+
76
+ window.makeKeyAndOrderFront(nil)
77
+ NSApp.activate(ignoringOtherApps: true)
78
+ }
79
+
80
+ func windowWillClose(_ notification: Notification) {
81
+ NSApp.terminate(nil)
82
+ }
83
+ }
84
+
85
+ let rawTarget = CommandLine.arguments.dropFirst().first ?? "http://127.0.0.1:4321"
86
+
87
+ guard let targetUrl = URL(string: rawTarget) else {
88
+ fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
89
+ exit(1)
90
+ }
91
+
92
+ let app = NSApplication.shared
93
+ app.setActivationPolicy(.regular)
94
+ let delegate = BrainlinkAppDelegate(targetUrl: targetUrl)
95
+ app.delegate = delegate
96
+ app.run()
97
+ `;
98
+ const nativeGuiPowershellScript = `param(
99
+ [string]$TargetUrl = "http://127.0.0.1:4321"
100
+ )
101
+
102
+ Add-Type -AssemblyName System.Windows.Forms
103
+ Add-Type -AssemblyName System.Drawing
104
+ [System.Windows.Forms.Application]::EnableVisualStyles()
105
+
106
+ $form = New-Object System.Windows.Forms.Form
107
+ $form.Text = "Brainlink Graph"
108
+ $form.Width = 1320
109
+ $form.Height = 860
110
+ $form.StartPosition = "CenterScreen"
111
+
112
+ $browser = New-Object System.Windows.Forms.WebBrowser
113
+ $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
114
+ $browser.ScriptErrorsSuppressed = $true
115
+ $browser.Navigate($TargetUrl)
116
+
117
+ $form.Controls.Add($browser)
118
+ [void]$form.ShowDialog()
119
+ `;
120
+ const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
121
+ import sys
122
+
123
+ def run() -> int:
124
+ try:
125
+ import gi
126
+ gi.require_version("Gtk", "3.0")
127
+ try:
128
+ gi.require_version("WebKit2", "4.1")
129
+ except ValueError:
130
+ gi.require_version("WebKit2", "4.0")
131
+ from gi.repository import Gtk, WebKit2
132
+ except Exception:
133
+ return 1
134
+
135
+ target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
136
+
137
+ window = Gtk.Window(title="Brainlink Graph")
138
+ window.set_default_size(1320, 860)
139
+ window.connect("destroy", Gtk.main_quit)
140
+
141
+ webview = WebKit2.WebView()
142
+ webview.load_uri(target_url)
143
+ window.add(webview)
144
+ window.show_all()
145
+ Gtk.main()
146
+ return 0
147
+
148
+ if __name__ == "__main__":
149
+ raise SystemExit(run())
150
+ `;
151
+ const commandExists = (command) => {
152
+ try {
153
+ const probe = platform() === 'win32'
154
+ ? spawnSync('where', [command], { stdio: 'ignore' })
155
+ : spawnSync('which', [command], { stdio: 'ignore' });
156
+ return probe.status === 0;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ };
162
+ const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
163
+ const resolveSwiftExecutable = () => {
164
+ const directSwift = '/usr/bin/swift';
165
+ if (existsSync(directSwift)) {
166
+ return directSwift;
167
+ }
168
+ try {
169
+ const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
170
+ const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
171
+ return swiftPath.length > 0 ? swiftPath : null;
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ };
177
+ const openGraphInMacNativeGui = (url) => {
178
+ const swiftBinary = resolveSwiftExecutable();
179
+ if (!swiftBinary) {
180
+ return false;
181
+ }
182
+ try {
183
+ writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url]);
189
+ };
190
+ const resolveWindowsPowershellExecutable = () => {
191
+ if (commandExists('powershell')) {
192
+ return 'powershell';
193
+ }
194
+ if (commandExists('pwsh')) {
195
+ return 'pwsh';
196
+ }
197
+ return null;
198
+ };
199
+ const openGraphInWindowsNativeGui = (url) => {
200
+ const powershell = resolveWindowsPowershellExecutable();
201
+ if (!powershell) {
202
+ return false;
203
+ }
204
+ try {
205
+ writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url]);
211
+ };
212
+ const openGraphInLinuxNativeGui = (url) => {
213
+ if (!commandExists('python3')) {
214
+ return false;
215
+ }
216
+ try {
217
+ writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ return spawnDetached('python3', [nativeGuiLinuxScriptPath, url]);
223
+ };
224
+ const openGraphInNativeGui = (url) => {
225
+ if (platform() === 'darwin') {
226
+ return openGraphInMacNativeGui(url);
227
+ }
228
+ if (platform() === 'win32') {
229
+ return openGraphInWindowsNativeGui(url);
230
+ }
231
+ return openGraphInLinuxNativeGui(url);
232
+ };
40
233
  const openGraphInAppWindow = (url) => {
41
234
  if (platform() === 'darwin') {
42
235
  const macCandidates = [
@@ -66,13 +259,19 @@ const openGraphInAppWindow = (url) => {
66
259
  spawnDetached('microsoft-edge', [appArgument, '--new-window']) ||
67
260
  spawnDetached('microsoft-edge-stable', [appArgument, '--new-window']));
68
261
  };
69
- const openUrlInBrowser = (url) => {
262
+ const openUrlInUi = (url) => {
70
263
  const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
71
264
  process.env.BRAINLINK_NO_BROWSER === 'true' ||
72
265
  process.env.CI === 'true';
73
266
  if (openDisabled) {
74
267
  return { opened: false, mode: 'none' };
75
268
  }
269
+ const currentPlatform = platform();
270
+ const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
271
+ (currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
272
+ if (nativeGuiEnabled && openGraphInNativeGui(url)) {
273
+ return { opened: true, mode: 'native-gui' };
274
+ }
76
275
  if (openGraphInAppWindow(url)) {
77
276
  return { opened: true, mode: 'app-window' };
78
277
  }
@@ -278,7 +477,7 @@ export const registerWriteCommands = (program) => {
278
477
  .option('-h, --host <host>', 'server host', '127.0.0.1')
279
478
  .option('-p, --port <port>', 'server port', '4321')
280
479
  .option('--no-index', 'skip indexing before starting the server')
281
- .option('--no-open', 'do not open the graph UI in the default browser')
480
+ .option('--no-open', 'do not open the graph UI automatically')
282
481
  .option('-w, --watch', 'watch markdown files and reindex on changes')
283
482
  .option('--json', 'print machine-readable JSON')
284
483
  .description('start a local web UI for the knowledge graph')
@@ -291,7 +490,7 @@ export const registerWriteCommands = (program) => {
291
490
  shouldIndex: options.index,
292
491
  shouldWatch: Boolean(options.watch)
293
492
  });
294
- const openResult = options.open !== false ? openUrlInBrowser(server.url) : { opened: false, mode: 'none' };
493
+ const openResult = options.open !== false ? openUrlInUi(server.url) : { opened: false, mode: 'none' };
295
494
  print(options.json, {
296
495
  url: server.url,
297
496
  watch: Boolean(options.watch),
@@ -299,9 +498,11 @@ export const registerWriteCommands = (program) => {
299
498
  openedUi: openResult.opened,
300
499
  openMode: openResult.mode
301
500
  }, () => `Brainlink graph server running at ${server.url}${openResult.opened
302
- ? openResult.mode === 'app-window'
303
- ? ' (opened in dedicated app window)'
304
- : ' (opened in browser)'
501
+ ? openResult.mode === 'native-gui'
502
+ ? ' (opened in native desktop GUI)'
503
+ : openResult.mode === 'app-window'
504
+ ? ' (opened in dedicated app window)'
505
+ : ' (opened in browser)'
305
506
  : options.open === false
306
507
  ? ' (auto-open disabled)'
307
508
  : ''}`);
@@ -540,7 +540,13 @@ blink server --vault ./vault --host 127.0.0.1 --port 4321 --no-open
540
540
  ```
541
541
 
542
542
  This starts a local frontend for inspecting the knowledge graph.
543
- By default it tries to open the graph in a dedicated app window and falls back to the default browser when app-window mode is unavailable.
543
+ By default it tries to open the graph in a native desktop GUI window:
544
+ - macOS: Swift + WebKit
545
+ - Windows: PowerShell WinForms WebBrowser
546
+ - Linux: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
547
+
548
+ On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
549
+ If native GUI launch is unavailable, it falls back to dedicated app-window mode and then to the default browser.
544
550
  Use `--no-open` to keep the server headless.
545
551
 
546
552
  Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.33",
3
+ "version": "0.1.0-beta.35",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",