beads-ui 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGES.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changes
2
2
 
3
+ ## 0.11.0
4
+
5
+ - [`fc00b87`](https://github.com/mantoni/beads-ui/commit/fc00b87cfd1b6600a9b9088a9f62c2f6e8fc919e)
6
+ fix(ui): harden daemon restart workspace registration (Leon Letto)
7
+ - [`2ea0dd0`](https://github.com/mantoni/beads-ui/commit/2ea0dd08eb71625fa3ae51e64ea6501b4d058154)
8
+ perf(ui): reduce list latency by default sandbox bd calls (Leon Letto)
9
+
10
+ _Released by [Maximilian Antoni](https://github.com/mantoni) on 2026-03-05._
11
+
3
12
  ## 0.10.1
4
13
 
5
14
  - [`62017f7`](https://github.com/mantoni/beads-ui/commit/62017f74fadb439c7270160ac03866d3554f36a3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beads-ui",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Local UI for Beads — Collaborate on issues with your coding agent.",
5
5
  "keywords": [
6
6
  "agent",
package/server/bd.js CHANGED
@@ -94,7 +94,7 @@ function runBdUnlocked(args, options = {}) {
94
94
  };
95
95
 
96
96
  /** @type {string[]} */
97
- const final_args = args.slice();
97
+ const final_args = buildBdArgs(args);
98
98
 
99
99
  return new Promise((resolve) => {
100
100
  const child = spawn(bin, final_args, spawn_opts);
@@ -151,6 +151,27 @@ function runBdUnlocked(args, options = {}) {
151
151
  });
152
152
  }
153
153
 
154
+ /**
155
+ * Build final bd CLI arguments.
156
+ * bdui defaults to sandbox mode to avoid sync/autopush overhead on interactive
157
+ * UI requests. Set `BDUI_BD_SANDBOX=0` (or "false") to opt out.
158
+ *
159
+ * @param {string[]} args
160
+ * @returns {string[]}
161
+ */
162
+ function buildBdArgs(args) {
163
+ const arg_set = new Set(args);
164
+ const raw_sandbox = String(process.env.BDUI_BD_SANDBOX || '').toLowerCase();
165
+ const sandbox_disabled = raw_sandbox === '0' || raw_sandbox === 'false';
166
+ const should_prepend_sandbox = !sandbox_disabled && !arg_set.has('--sandbox');
167
+
168
+ if (!should_prepend_sandbox) {
169
+ return args.slice();
170
+ }
171
+
172
+ return ['--sandbox', ...args];
173
+ }
174
+
154
175
  /**
155
176
  * Serialize `bd` invocations.
156
177
  * Dolt embedded mode can crash when multiple `bd` processes run concurrently
@@ -10,6 +10,10 @@ import {
10
10
  } from './daemon.js';
11
11
  import { openUrl, registerWorkspaceWithServer, waitForServer } from './open.js';
12
12
 
13
+ const STARTUP_SETTLE_MS = 200;
14
+ const REGISTER_RETRY_ATTEMPTS = 5;
15
+ const REGISTER_RETRY_DELAY_MS = 150;
16
+
13
17
  /**
14
18
  * Handle `start` command. Idempotent when already running.
15
19
  * - Spawns a detached server process, writes PID file, returns 0.
@@ -21,27 +25,17 @@ import { openUrl, registerWorkspaceWithServer, waitForServer } from './open.js';
21
25
  export async function handleStart(options) {
22
26
  // Default: do not open a browser unless explicitly requested via `open: true`.
23
27
  const should_open = options?.open === true;
28
+ const cwd = process.cwd();
24
29
  const existing_pid = readPidFile();
25
30
  if (existing_pid && isProcessRunning(existing_pid)) {
26
31
  // Server is already running - register this workspace dynamically
27
- const cwd = process.cwd();
28
- const workspace_database = resolveWorkspaceDatabase({ cwd });
29
- if (
30
- workspace_database.source !== 'home-default' &&
31
- workspace_database.exists
32
- ) {
33
- const { url } = getConfig();
34
- const registered = await registerWorkspaceWithServer(url, {
35
- path: cwd,
36
- database: workspace_database.path
37
- });
38
- if (registered) {
39
- console.log('Workspace registered: %s', cwd);
40
- }
32
+ const { url } = getConfig();
33
+ const registered = await registerCurrentWorkspace(url, cwd);
34
+ if (registered) {
35
+ console.log('Workspace registered: %s', cwd);
41
36
  }
42
37
  console.warn('Server is already running.');
43
38
  if (should_open) {
44
- const { url } = getConfig();
45
39
  await openUrl(url);
46
40
  }
47
41
  return 0;
@@ -58,6 +52,7 @@ export async function handleStart(options) {
58
52
  if (options?.port) {
59
53
  process.env.PORT = String(options.port);
60
54
  }
55
+ const { url } = getConfig();
61
56
 
62
57
  const started = startDaemon({
63
58
  is_debug: options?.is_debug,
@@ -65,10 +60,32 @@ export async function handleStart(options) {
65
60
  port: options?.port
66
61
  });
67
62
  if (started && started.pid > 0) {
63
+ // Give the spawned daemon a brief moment to fail fast (for example EADDRINUSE).
64
+ await sleep(STARTUP_SETTLE_MS);
65
+
66
+ if (!isProcessRunning(started.pid)) {
67
+ removePidFile();
68
+
69
+ // If another server is already running at the configured URL, register this
70
+ // workspace there so it appears in the picker instead of silently missing.
71
+ const registered = await registerCurrentWorkspaceWithRetry(url, cwd);
72
+ if (registered) {
73
+ console.warn(
74
+ 'Daemon exited early; registered workspace with existing server: %s',
75
+ cwd
76
+ );
77
+ return 0;
78
+ }
79
+ return 1;
80
+ }
81
+
82
+ // Register against the currently reachable server to ensure this workspace
83
+ // appears in the picker even when startup races with other daemons.
84
+ void registerCurrentWorkspaceWithRetry(url, cwd);
85
+
68
86
  printServerUrl();
69
87
  // Auto-open the browser once for a fresh daemon start
70
88
  if (should_open) {
71
- const { url } = getConfig();
72
89
  // Wait briefly for the server to accept connections (single retry window)
73
90
  await waitForServer(url, 600);
74
91
  // Best-effort open; ignore result
@@ -80,6 +97,56 @@ export async function handleStart(options) {
80
97
  return 1;
81
98
  }
82
99
 
100
+ /**
101
+ * @param {number} ms
102
+ * @returns {Promise<void>}
103
+ */
104
+ function sleep(ms) {
105
+ return new Promise((resolve) => {
106
+ setTimeout(() => {
107
+ resolve();
108
+ }, ms);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * @param {string} url
114
+ * @param {string} cwd
115
+ * @returns {Promise<boolean>}
116
+ */
117
+ async function registerCurrentWorkspace(url, cwd) {
118
+ const workspace_database = resolveWorkspaceDatabase({ cwd });
119
+ if (
120
+ workspace_database.source === 'home-default' ||
121
+ !workspace_database.exists
122
+ ) {
123
+ return false;
124
+ }
125
+
126
+ return registerWorkspaceWithServer(url, {
127
+ path: cwd,
128
+ database: workspace_database.path
129
+ });
130
+ }
131
+
132
+ /**
133
+ * @param {string} url
134
+ * @param {string} cwd
135
+ * @returns {Promise<boolean>}
136
+ */
137
+ async function registerCurrentWorkspaceWithRetry(url, cwd) {
138
+ for (let i = 0; i < REGISTER_RETRY_ATTEMPTS; i++) {
139
+ const registered = await registerCurrentWorkspace(url, cwd);
140
+ if (registered) {
141
+ return true;
142
+ }
143
+ if (i < REGISTER_RETRY_ATTEMPTS - 1) {
144
+ await sleep(REGISTER_RETRY_DELAY_MS);
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
83
150
  /**
84
151
  * Handle `stop` command.
85
152
  * - Sends SIGTERM and waits for exit (with SIGKILL fallback), removes PID file.