bosun 0.36.5 → 0.36.7
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/bosun.schema.json +49 -0
- package/desktop/desktop-shortcuts.mjs +424 -0
- package/desktop/main.mjs +56 -10
- package/desktop/preload.cjs +26 -0
- package/desktop-api-key.mjs +152 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +107 -2
- package/ui/app.js +44 -25
- package/ui/components/agent-selector.js +8 -8
- package/ui/components/chat-view.js +3 -3
- package/ui/modules/router.js +26 -6
- package/ui/modules/settings-schema.js +5 -5
- package/ui/setup.html +419 -60
- package/ui/tabs/agents.js +16 -9
- package/ui/tabs/infra.js +1 -1
- package/ui/tabs/library.js +1 -1
- package/ui/tabs/settings.js +446 -10
- package/ui/tabs/tasks.js +6 -6
- package/ui-server.mjs +221 -1
- package/voice-agents-sdk.mjs +10 -1
- package/voice-auth-manager.mjs +350 -0
- package/voice-relay.mjs +121 -30
package/bosun.schema.json
CHANGED
|
@@ -296,6 +296,55 @@
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
},
|
|
299
|
+
"voiceEndpoints": {
|
|
300
|
+
"type": "array",
|
|
301
|
+
"description": "Named realtime voice endpoints, each with its own credentials. Multiple endpoints enable automatic failover with context preservation.",
|
|
302
|
+
"items": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"additionalProperties": false,
|
|
305
|
+
"required": ["provider"],
|
|
306
|
+
"properties": {
|
|
307
|
+
"name": {
|
|
308
|
+
"type": "string",
|
|
309
|
+
"description": "Friendly label for this endpoint"
|
|
310
|
+
},
|
|
311
|
+
"provider": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"enum": ["azure", "openai"],
|
|
314
|
+
"description": "Provider type"
|
|
315
|
+
},
|
|
316
|
+
"endpoint": {
|
|
317
|
+
"type": "string",
|
|
318
|
+
"description": "Azure OpenAI endpoint URL (required for azure)"
|
|
319
|
+
},
|
|
320
|
+
"deployment": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "Azure deployment name (required for azure)"
|
|
323
|
+
},
|
|
324
|
+
"apiKey": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"description": "API key for this endpoint"
|
|
327
|
+
},
|
|
328
|
+
"model": {
|
|
329
|
+
"type": "string",
|
|
330
|
+
"description": "Model name (for openai provider)"
|
|
331
|
+
},
|
|
332
|
+
"voiceId": { "type": "string" },
|
|
333
|
+
"visionModel": { "type": "string" },
|
|
334
|
+
"role": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"enum": ["primary", "backup"],
|
|
337
|
+
"default": "primary"
|
|
338
|
+
},
|
|
339
|
+
"weight": {
|
|
340
|
+
"type": "number",
|
|
341
|
+
"default": 100,
|
|
342
|
+
"description": "Priority weight — higher = tried first"
|
|
343
|
+
},
|
|
344
|
+
"enabled": { "type": "boolean", "default": true }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
299
348
|
"delegateExecutor": {
|
|
300
349
|
"type": "string",
|
|
301
350
|
"enum": [
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* desktop-shortcuts.mjs
|
|
3
|
+
*
|
|
4
|
+
* Keyboard shortcut manager for the Bosun desktop app.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Defines the canonical catalog of every available shortcut.
|
|
8
|
+
* - Persists user customizations to {configDir}/desktop-shortcuts.json.
|
|
9
|
+
* - Registers / unregisters Electron global shortcuts.
|
|
10
|
+
* - Provides the IPC payload for the settings UI.
|
|
11
|
+
* - Detects accelerator conflicts before applying changes.
|
|
12
|
+
*
|
|
13
|
+
* Scopes
|
|
14
|
+
* - "global" : fires system-wide even when the app is in the background.
|
|
15
|
+
* - "local" : fires only when the app is focused (wired as menu accelerators).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { globalShortcut } from "electron";
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
|
|
22
|
+
// ── Scope constants ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Fires system-wide, even when Bosun is in the background. */
|
|
25
|
+
export const SCOPE_GLOBAL = "global";
|
|
26
|
+
|
|
27
|
+
/** Fires only when the Bosun window is focused (menu accelerator). */
|
|
28
|
+
export const SCOPE_LOCAL = "local";
|
|
29
|
+
|
|
30
|
+
// ── Default catalog ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} ShortcutDef
|
|
34
|
+
* @property {string} id Unique identifier used as a key.
|
|
35
|
+
* @property {string} label Human-readable name shown in UI.
|
|
36
|
+
* @property {string} description Longer description for tooltips/help.
|
|
37
|
+
* @property {string} defaultAccelerator Default Electron accelerator string.
|
|
38
|
+
* @property {string} scope "global" | "local"
|
|
39
|
+
* @property {string} [group] Optional display grouping.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** @type {ShortcutDef[]} */
|
|
43
|
+
export const DEFAULT_SHORTCUTS = [
|
|
44
|
+
// ── Global ─ fire from anywhere on the desktop ────────────────────────────
|
|
45
|
+
{
|
|
46
|
+
id: "bosun.focus",
|
|
47
|
+
label: "Focus Bosun",
|
|
48
|
+
description: "Bring the Bosun window to front from anywhere on your desktop",
|
|
49
|
+
defaultAccelerator: "CmdOrCtrl+Shift+B",
|
|
50
|
+
scope: SCOPE_GLOBAL,
|
|
51
|
+
group: "Global",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "bosun.quickchat",
|
|
55
|
+
label: "Quick New Chat",
|
|
56
|
+
description: "Focus Bosun and immediately open a brand-new chat session",
|
|
57
|
+
defaultAccelerator: "CmdOrCtrl+Shift+N",
|
|
58
|
+
scope: SCOPE_GLOBAL,
|
|
59
|
+
group: "Global",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "bosun.voice.call",
|
|
63
|
+
label: "Start Voice Call",
|
|
64
|
+
description: "Open the voice companion and start a voice call",
|
|
65
|
+
defaultAccelerator: "CmdOrCtrl+Shift+Space",
|
|
66
|
+
scope: SCOPE_GLOBAL,
|
|
67
|
+
group: "Global",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "bosun.voice.video",
|
|
71
|
+
label: "Start Video Call",
|
|
72
|
+
description: "Open the voice companion and start a video call",
|
|
73
|
+
defaultAccelerator: "CmdOrCtrl+Shift+K",
|
|
74
|
+
scope: SCOPE_GLOBAL,
|
|
75
|
+
group: "Global",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "bosun.voice.toggle",
|
|
79
|
+
label: "Toggle Voice Companion",
|
|
80
|
+
description: "Show or hide the floating voice companion window",
|
|
81
|
+
defaultAccelerator: "CmdOrCtrl+Shift+V",
|
|
82
|
+
scope: SCOPE_GLOBAL,
|
|
83
|
+
group: "Global",
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// ── Local ─ menu accelerators, only when app is focused ───────────────────
|
|
87
|
+
{
|
|
88
|
+
id: "app.newchat",
|
|
89
|
+
label: "New Chat",
|
|
90
|
+
description: "Open a new chat session",
|
|
91
|
+
defaultAccelerator: "CmdOrCtrl+N",
|
|
92
|
+
scope: SCOPE_LOCAL,
|
|
93
|
+
group: "File",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "app.settings",
|
|
97
|
+
label: "Preferences",
|
|
98
|
+
description: "Open the Bosun settings panel",
|
|
99
|
+
defaultAccelerator: "CmdOrCtrl+,",
|
|
100
|
+
scope: SCOPE_LOCAL,
|
|
101
|
+
group: "File",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "bosun.navigate.home",
|
|
105
|
+
label: "Dashboard",
|
|
106
|
+
description: "Navigate to the main Bosun dashboard",
|
|
107
|
+
defaultAccelerator: "CmdOrCtrl+H",
|
|
108
|
+
scope: SCOPE_LOCAL,
|
|
109
|
+
group: "Navigation",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "bosun.navigate.agents",
|
|
113
|
+
label: "Agents",
|
|
114
|
+
description: "Navigate to the Agents panel",
|
|
115
|
+
defaultAccelerator: "CmdOrCtrl+Shift+A",
|
|
116
|
+
scope: SCOPE_LOCAL,
|
|
117
|
+
group: "Navigation",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "bosun.navigate.tasks",
|
|
121
|
+
label: "Tasks",
|
|
122
|
+
description: "Navigate to the Tasks panel",
|
|
123
|
+
defaultAccelerator: "CmdOrCtrl+Shift+T",
|
|
124
|
+
scope: SCOPE_LOCAL,
|
|
125
|
+
group: "Navigation",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "bosun.navigate.logs",
|
|
129
|
+
label: "Logs",
|
|
130
|
+
description: "Navigate to the Logs panel",
|
|
131
|
+
defaultAccelerator: "CmdOrCtrl+Shift+L",
|
|
132
|
+
scope: SCOPE_LOCAL,
|
|
133
|
+
group: "Navigation",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "bosun.navigate.settings",
|
|
137
|
+
label: "Settings",
|
|
138
|
+
description: "Navigate to the Settings panel",
|
|
139
|
+
defaultAccelerator: "CmdOrCtrl+Shift+S",
|
|
140
|
+
scope: SCOPE_LOCAL,
|
|
141
|
+
group: "Navigation",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "bosun.show.shortcuts",
|
|
145
|
+
label: "Show Keyboard Shortcuts",
|
|
146
|
+
description: "Display a reference sheet of all keyboard shortcuts",
|
|
147
|
+
defaultAccelerator: "CmdOrCtrl+/",
|
|
148
|
+
scope: SCOPE_LOCAL,
|
|
149
|
+
group: "Help",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// ── Module state ─────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/** @type {Map<string, () => void>} */
|
|
156
|
+
const actionHandlers = new Map();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* User customizations: Map<id, accelerator | null>.
|
|
160
|
+
* null = explicitly disabled.
|
|
161
|
+
*/
|
|
162
|
+
let customizations = new Map();
|
|
163
|
+
|
|
164
|
+
/** Path to the JSON config file. */
|
|
165
|
+
let configFilePath = null;
|
|
166
|
+
|
|
167
|
+
/** Whether global shortcuts are currently registered. */
|
|
168
|
+
let globalsActive = false;
|
|
169
|
+
|
|
170
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Initialise the shortcuts manager.
|
|
174
|
+
* Must be called before registerGlobalShortcuts().
|
|
175
|
+
*
|
|
176
|
+
* @param {string} configDir The bosun config directory path.
|
|
177
|
+
*/
|
|
178
|
+
export function initShortcuts(configDir) {
|
|
179
|
+
configFilePath = resolve(configDir, "desktop-shortcuts.json");
|
|
180
|
+
customizations = _loadCustomizations(configFilePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register a callback for a shortcut action.
|
|
185
|
+
* Handlers should be registered before calling registerGlobalShortcuts().
|
|
186
|
+
*
|
|
187
|
+
* @param {string} id Shortcut ID from DEFAULT_SHORTCUTS.
|
|
188
|
+
* @param {() => void} handler Function to invoke when the shortcut fires.
|
|
189
|
+
*/
|
|
190
|
+
export function onShortcut(id, handler) {
|
|
191
|
+
actionHandlers.set(id, handler);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Register all SCOPE_GLOBAL shortcuts with Electron.
|
|
196
|
+
* Silently skips shortcuts that have no registered action handler.
|
|
197
|
+
* Safe to call multiple times — old registrations are cleared first.
|
|
198
|
+
*/
|
|
199
|
+
export function registerGlobalShortcuts() {
|
|
200
|
+
// Unregister all previously registered globals cleanly.
|
|
201
|
+
_unregisterAllGlobals();
|
|
202
|
+
|
|
203
|
+
for (const def of DEFAULT_SHORTCUTS) {
|
|
204
|
+
if (def.scope !== SCOPE_GLOBAL) continue;
|
|
205
|
+
|
|
206
|
+
const accelerator = getEffectiveAccelerator(def.id);
|
|
207
|
+
if (!accelerator) continue; // disabled by user
|
|
208
|
+
|
|
209
|
+
const handler = actionHandlers.get(def.id);
|
|
210
|
+
if (!handler) continue; // no action wired yet
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const ok = globalShortcut.register(accelerator, handler);
|
|
214
|
+
if (!ok) {
|
|
215
|
+
console.warn(
|
|
216
|
+
`[shortcuts] global shortcut already in use: ${accelerator} (${def.id})`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.warn(
|
|
221
|
+
`[shortcuts] failed to register ${accelerator} (${def.id}):`,
|
|
222
|
+
err?.message || err,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
globalsActive = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Unregister all global shortcuts managed by this module.
|
|
232
|
+
* Called during app shutdown.
|
|
233
|
+
*/
|
|
234
|
+
export function unregisterGlobalShortcuts() {
|
|
235
|
+
_unregisterAllGlobals();
|
|
236
|
+
globalsActive = false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Return the complete shortcuts list merged with user customizations.
|
|
241
|
+
* This is the payload delivered over IPC to the settings UI.
|
|
242
|
+
*
|
|
243
|
+
* @returns {Array<ShortcutDef & { accelerator: string|null, isCustomized: boolean, isDisabled: boolean }>}
|
|
244
|
+
*/
|
|
245
|
+
export function getAllShortcuts() {
|
|
246
|
+
return DEFAULT_SHORTCUTS.map((def) => {
|
|
247
|
+
const hasOverride = customizations.has(def.id);
|
|
248
|
+
const overrideValue = customizations.get(def.id);
|
|
249
|
+
return {
|
|
250
|
+
id: def.id,
|
|
251
|
+
label: def.label,
|
|
252
|
+
description: def.description,
|
|
253
|
+
defaultAccelerator: def.defaultAccelerator,
|
|
254
|
+
accelerator: hasOverride
|
|
255
|
+
? (overrideValue ?? null)
|
|
256
|
+
: def.defaultAccelerator,
|
|
257
|
+
scope: def.scope,
|
|
258
|
+
group: def.group ?? "",
|
|
259
|
+
isCustomized: hasOverride && overrideValue !== undefined,
|
|
260
|
+
isDisabled: hasOverride && overrideValue === null,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the effective (possibly customized) accelerator for a shortcut.
|
|
267
|
+
* Returns null if the shortcut has been explicitly disabled.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} id
|
|
270
|
+
* @returns {string|null}
|
|
271
|
+
*/
|
|
272
|
+
export function getEffectiveAccelerator(id) {
|
|
273
|
+
if (customizations.has(id)) {
|
|
274
|
+
const v = customizations.get(id);
|
|
275
|
+
return v === null ? null : v;
|
|
276
|
+
}
|
|
277
|
+
return DEFAULT_SHORTCUTS.find((d) => d.id === id)?.defaultAccelerator ?? null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Set a custom accelerator for a shortcut.
|
|
282
|
+
* Pass `null` to disable the shortcut entirely.
|
|
283
|
+
* Automatically re-registers global shortcuts when needed.
|
|
284
|
+
*
|
|
285
|
+
* @param {string} id
|
|
286
|
+
* @param {string|null} accelerator Electron accelerator string, or null to disable.
|
|
287
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
288
|
+
*/
|
|
289
|
+
export function setShortcut(id, accelerator) {
|
|
290
|
+
const def = DEFAULT_SHORTCUTS.find((d) => d.id === id);
|
|
291
|
+
if (!def) return { ok: false, error: `Unknown shortcut: ${id}` };
|
|
292
|
+
|
|
293
|
+
if (accelerator !== null && accelerator !== undefined) {
|
|
294
|
+
// Reject empty strings
|
|
295
|
+
if (!String(accelerator).trim()) {
|
|
296
|
+
return { ok: false, error: "Accelerator must not be empty. Pass null to disable." };
|
|
297
|
+
}
|
|
298
|
+
// Conflict check — only among shortcuts of the same scope
|
|
299
|
+
const conflict = _findConflict(id, accelerator, def.scope);
|
|
300
|
+
if (conflict) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: `'${accelerator}' is already used by '${conflict}'`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
customizations.set(id, accelerator === undefined ? null : accelerator);
|
|
309
|
+
_saveCustomizations(configFilePath, customizations);
|
|
310
|
+
|
|
311
|
+
// Re-apply globals when a global shortcut is changed.
|
|
312
|
+
if (def.scope === SCOPE_GLOBAL && globalsActive) {
|
|
313
|
+
registerGlobalShortcuts();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { ok: true };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Reset a shortcut to its default accelerator.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} id
|
|
323
|
+
* @returns {{ ok: boolean }}
|
|
324
|
+
*/
|
|
325
|
+
export function resetShortcut(id) {
|
|
326
|
+
const def = DEFAULT_SHORTCUTS.find((d) => d.id === id);
|
|
327
|
+
if (!def) return { ok: false, error: `Unknown shortcut: ${id}` };
|
|
328
|
+
|
|
329
|
+
customizations.delete(id);
|
|
330
|
+
_saveCustomizations(configFilePath, customizations);
|
|
331
|
+
|
|
332
|
+
if (def.scope === SCOPE_GLOBAL && globalsActive) {
|
|
333
|
+
registerGlobalShortcuts();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { ok: true };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Reset ALL shortcuts to their defaults.
|
|
341
|
+
*
|
|
342
|
+
* @returns {{ ok: boolean }}
|
|
343
|
+
*/
|
|
344
|
+
export function resetAllShortcuts() {
|
|
345
|
+
customizations.clear();
|
|
346
|
+
_saveCustomizations(configFilePath, customizations);
|
|
347
|
+
|
|
348
|
+
if (globalsActive) registerGlobalShortcuts();
|
|
349
|
+
return { ok: true };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
function _unregisterAllGlobals() {
|
|
355
|
+
for (const def of DEFAULT_SHORTCUTS) {
|
|
356
|
+
if (def.scope !== SCOPE_GLOBAL) continue;
|
|
357
|
+
|
|
358
|
+
// Unregister both the currently effective AND the default to be safe
|
|
359
|
+
// when an override is being replaced.
|
|
360
|
+
const current = getEffectiveAccelerator(def.id);
|
|
361
|
+
const fallback = def.defaultAccelerator;
|
|
362
|
+
|
|
363
|
+
for (const acc of new Set([current, fallback])) {
|
|
364
|
+
if (acc) {
|
|
365
|
+
try {
|
|
366
|
+
globalShortcut.unregister(acc);
|
|
367
|
+
} catch {
|
|
368
|
+
/* best effort */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Find the label of a shortcut in the same scope that already uses `accelerator`.
|
|
377
|
+
* Returns null if there is no conflict.
|
|
378
|
+
*
|
|
379
|
+
* @param {string} excludeId
|
|
380
|
+
* @param {string} accelerator
|
|
381
|
+
* @param {string} scope
|
|
382
|
+
* @returns {string|null}
|
|
383
|
+
*/
|
|
384
|
+
function _findConflict(excludeId, accelerator, scope) {
|
|
385
|
+
const normalized = _normalizeAcc(accelerator);
|
|
386
|
+
for (const def of DEFAULT_SHORTCUTS) {
|
|
387
|
+
if (def.id === excludeId) continue;
|
|
388
|
+
if (def.scope !== scope) continue;
|
|
389
|
+
const eff = getEffectiveAccelerator(def.id);
|
|
390
|
+
if (eff && _normalizeAcc(eff) === normalized) {
|
|
391
|
+
return def.label;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function _normalizeAcc(acc) {
|
|
398
|
+
return String(acc).toLowerCase().replace(/\s+/g, "");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function _loadCustomizations(filePath) {
|
|
402
|
+
try {
|
|
403
|
+
if (!existsSync(filePath)) return new Map();
|
|
404
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
405
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return new Map();
|
|
406
|
+
return new Map(
|
|
407
|
+
Object.entries(raw).map(([k, v]) => [k, v === null ? null : String(v)]),
|
|
408
|
+
);
|
|
409
|
+
} catch {
|
|
410
|
+
return new Map();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function _saveCustomizations(filePath, map) {
|
|
415
|
+
if (!filePath) return;
|
|
416
|
+
try {
|
|
417
|
+
const dir = dirname(filePath);
|
|
418
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
419
|
+
const obj = Object.fromEntries(map);
|
|
420
|
+
writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf8");
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.warn("[shortcuts] failed to save:", err?.message || err);
|
|
423
|
+
}
|
|
424
|
+
}
|
package/desktop/main.mjs
CHANGED
|
@@ -16,7 +16,55 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
16
16
|
import { request as httpRequest } from "node:http";
|
|
17
17
|
import { request as httpsRequest } from "node:https";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
function createUnavailableShortcutsApi() {
|
|
23
|
+
const unavailable = () => ({
|
|
24
|
+
ok: false,
|
|
25
|
+
error: "Keyboard shortcuts module is unavailable in this installation. Reinstall Bosun.",
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
initShortcuts: () => {},
|
|
29
|
+
onShortcut: () => {},
|
|
30
|
+
getAllShortcuts: () => [],
|
|
31
|
+
getEffectiveAccelerator: () => null,
|
|
32
|
+
registerGlobalShortcuts: () => {},
|
|
33
|
+
unregisterGlobalShortcuts: () => {
|
|
34
|
+
try {
|
|
35
|
+
globalShortcut.unregisterAll();
|
|
36
|
+
} catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
setShortcut: unavailable,
|
|
41
|
+
resetShortcut: unavailable,
|
|
42
|
+
resetAllShortcuts: unavailable,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isMissingModuleError(error) {
|
|
47
|
+
return Boolean(error && error.code === "ERR_MODULE_NOT_FOUND");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadShortcutsApi() {
|
|
51
|
+
const candidates = ["./desktop-shortcuts.mjs", "../desktop-shortcuts.mjs"];
|
|
52
|
+
for (const specifier of candidates) {
|
|
53
|
+
try {
|
|
54
|
+
return await import(specifier);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isMissingModuleError(error)) continue;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.warn(
|
|
62
|
+
"[desktop] keyboard shortcuts module not found; continuing with limited shortcut support",
|
|
63
|
+
);
|
|
64
|
+
return createUnavailableShortcutsApi();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const {
|
|
20
68
|
initShortcuts,
|
|
21
69
|
onShortcut,
|
|
22
70
|
getAllShortcuts,
|
|
@@ -26,9 +74,7 @@ import {
|
|
|
26
74
|
setShortcut,
|
|
27
75
|
resetShortcut,
|
|
28
76
|
resetAllShortcuts,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
77
|
+
} = await loadShortcutsApi();
|
|
32
78
|
|
|
33
79
|
process.title = "bosun-desktop";
|
|
34
80
|
|
|
@@ -613,7 +659,7 @@ async function createMainWindow() {
|
|
|
613
659
|
contextIsolation: true,
|
|
614
660
|
nodeIntegration: false,
|
|
615
661
|
sandbox: true,
|
|
616
|
-
preload: join(__dirname, "preload.
|
|
662
|
+
preload: join(__dirname, "preload.cjs"),
|
|
617
663
|
},
|
|
618
664
|
});
|
|
619
665
|
|
|
@@ -708,7 +754,7 @@ async function createFollowWindow() {
|
|
|
708
754
|
nodeIntegration: false,
|
|
709
755
|
sandbox: true,
|
|
710
756
|
backgroundThrottling: false,
|
|
711
|
-
preload: join(__dirname, "preload.
|
|
757
|
+
preload: join(__dirname, "preload.cjs"),
|
|
712
758
|
},
|
|
713
759
|
});
|
|
714
760
|
|
|
@@ -799,7 +845,7 @@ function refreshTrayMenu() {
|
|
|
799
845
|
{ type: /** @type {const} */ ("separator") },
|
|
800
846
|
{
|
|
801
847
|
label: "Voice Companion",
|
|
802
|
-
accelerator:
|
|
848
|
+
accelerator: acc("bosun.voice.toggle"),
|
|
803
849
|
click: () => {
|
|
804
850
|
if (!restoreFollowWindow()) setWindowVisible(mainWindow);
|
|
805
851
|
},
|
|
@@ -1139,10 +1185,10 @@ async function bootstrap() {
|
|
|
1139
1185
|
|
|
1140
1186
|
// Determine tray / background mode before creating any windows.
|
|
1141
1187
|
trayMode = isTrayModeEnabled();
|
|
1142
|
-
// In tray mode the window
|
|
1143
|
-
// Set BOSUN_DESKTOP_START_HIDDEN=
|
|
1188
|
+
// In tray mode we still open the main window by default on startup.
|
|
1189
|
+
// Set BOSUN_DESKTOP_START_HIDDEN=1 to force background-only launch.
|
|
1144
1190
|
startHidden = trayMode
|
|
1145
|
-
? parseBoolEnv(process.env.BOSUN_DESKTOP_START_HIDDEN,
|
|
1191
|
+
? parseBoolEnv(process.env.BOSUN_DESKTOP_START_HIDDEN, false)
|
|
1146
1192
|
: false;
|
|
1147
1193
|
|
|
1148
1194
|
if (trayMode) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld("veDesktop", {
|
|
4
|
+
platform: process.platform,
|
|
5
|
+
|
|
6
|
+
follow: {
|
|
7
|
+
open: async (detail = {}) => {
|
|
8
|
+
return ipcRenderer.invoke("bosun:desktop:follow:open", detail || {});
|
|
9
|
+
},
|
|
10
|
+
hide: async () => {
|
|
11
|
+
return ipcRenderer.invoke("bosun:desktop:follow:hide");
|
|
12
|
+
},
|
|
13
|
+
restore: async () => {
|
|
14
|
+
return ipcRenderer.invoke("bosun:desktop:follow:restore");
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
shortcuts: {
|
|
19
|
+
list: () => ipcRenderer.invoke("bosun:shortcuts:list"),
|
|
20
|
+
set: (id, accelerator) =>
|
|
21
|
+
ipcRenderer.invoke("bosun:shortcuts:set", { id, accelerator }),
|
|
22
|
+
reset: (id) => ipcRenderer.invoke("bosun:shortcuts:reset", { id }),
|
|
23
|
+
resetAll: () => ipcRenderer.invoke("bosun:shortcuts:resetAll"),
|
|
24
|
+
showDialog: () => ipcRenderer.invoke("bosun:shortcuts:showDialog"),
|
|
25
|
+
},
|
|
26
|
+
});
|