tide-commander 1.35.0 → 1.36.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.
Files changed (54) hide show
  1. package/dist/assets/{BossLogsModal-B_dgVF7L.js → BossLogsModal-B6meUM1T.js} +1 -1
  2. package/dist/assets/BossSpawnModal-DzWs1NNZ.js +1 -0
  3. package/dist/assets/{ControlsModal-WMTTqbca.js → ControlsModal-D2QDZ20h.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-THzhLHch.js → DockerLogsModal-Bh1ghgFV.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DWLKJYav.js → EmbeddedEditor-C4rKc9Ys.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-DN9ceaS6.js → GmailOAuthSetup-E5_WFtyX.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-bVST2EOB.js → GoogleOAuthSetup-B7NLxSSp.js} +1 -1
  8. package/dist/assets/{IframeModal-BELsjvgi.js → IframeModal-CGbINL-o.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-DwDr4BRt.js → IntegrationsPanel-DZmbjiXV.js} +2 -2
  10. package/dist/assets/{LogViewerModal-CMe04PO5.js → LogViewerModal-KI2L074n.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CqSalNeY.js → MonitoringModal-CWqjvTra.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-CCmCDxVt.js → PM2LogsModal-DRHybuUO.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-IfzPidIv.js → RestoreArchivedAreaModal-DjpKSz7P.js} +1 -1
  14. package/dist/assets/{SaveSnapshotModal-DUhrVD5l.js → SaveSnapshotModal-CWtf3rgg.js} +1 -1
  15. package/dist/assets/Scene2DCanvas-CSfRDtXw.js +1 -0
  16. package/dist/assets/SceneManager-Do4NpOsy.js +104 -0
  17. package/dist/assets/{SkillsPanel-CPFOI4Tl.js → SkillsPanel-BqZSNfNW.js} +3 -3
  18. package/dist/assets/{SnapshotManager-Cbu0tJBz.js → SnapshotManager-DyqyIhWz.js} +1 -1
  19. package/dist/assets/SpawnModal-CGFj_b9I.js +1 -0
  20. package/dist/assets/SubordinateAssignmentModal-DGFoupVt.js +1 -0
  21. package/dist/assets/{SupervisorPanel-BvX-dlk_.js → SupervisorPanel-B1PVopnm.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-RUVFmKmf.js → TriggerManagerPanel-CFez7_vm.js} +2 -2
  23. package/dist/assets/{WorkflowEditorPanel-CwZpEqzM.js → WorkflowEditorPanel-ClMl5U8g.js} +1 -1
  24. package/dist/assets/{index-DDPUtz8-.js → index-B4SUGrVB.js} +1 -1
  25. package/dist/assets/{index-CiD1Rwaq.js → index-BK3kI8Cb.js} +1 -1
  26. package/dist/assets/{index-BFguOWBW.js → index-Bf3KKyiV.js} +2 -2
  27. package/dist/assets/{index-D4nfDvz4.js → index-Bmu_CPsU.js} +3 -3
  28. package/dist/assets/index-CbzHGOPP.js +1 -0
  29. package/dist/assets/index-CpZt7iSV.js +1 -0
  30. package/dist/assets/{index-B-wV06cR.js → index-CxtzptPF.js} +5 -5
  31. package/dist/assets/{index-EH8IBvSU.js → index-D2_-BBP8.js} +3 -3
  32. package/dist/assets/main-C6uuUPJ0.css +1 -0
  33. package/dist/assets/main-CtGyoZXd.js +152 -0
  34. package/dist/assets/{web-D1vWYL8u.js → web-C2JUUvGB.js} +1 -1
  35. package/dist/assets/{web-DUq3Undh.js → web-D4EcUHSn.js} +1 -1
  36. package/dist/index.html +2 -2
  37. package/dist/src/packages/server/routes/agents.js +209 -1
  38. package/dist/src/packages/server/routes/areas.js +25 -0
  39. package/dist/src/packages/server/routes/index.js +2 -0
  40. package/dist/src/packages/server/routes/workspaces.js +123 -0
  41. package/dist/src/packages/server/services/agent-service.js +87 -1
  42. package/dist/src/packages/server/services/area-layout-service.js +260 -0
  43. package/dist/src/packages/server/services/workspace-service.js +104 -0
  44. package/package.json +1 -1
  45. package/dist/assets/BossSpawnModal-CSO1bYxA.js +0 -1
  46. package/dist/assets/Scene2DCanvas-Bl5DUC7w.js +0 -1
  47. package/dist/assets/SceneManager-BGO9tiaI.js +0 -104
  48. package/dist/assets/SpawnModal-BqDbsYLY.js +0 -1
  49. package/dist/assets/SubordinateAssignmentModal-DOqkhL_L.js +0 -1
  50. package/dist/assets/camera-D_KeL_pz.js +0 -1
  51. package/dist/assets/index-C7gqY2AA.js +0 -1
  52. package/dist/assets/index-H0PzHVFw.js +0 -1
  53. package/dist/assets/main-Cjm0d8dZ.js +0 -152
  54. package/dist/assets/main-DqC9_fF4.css +0 -1
@@ -1 +1 @@
1
- import{bp as s}from"./main-Cjm0d8dZ.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
1
+ import{bw as s}from"./main-CtGyoZXd.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class f extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{f as LocalNotificationsWeb};
@@ -1 +1 @@
1
- import{bp as a}from"./main-Cjm0d8dZ.js";import{ImpactStyle as i,NotificationType as r}from"./index-BFguOWBW.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class u extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{u as HapticsWeb};
1
+ import{bw as a}from"./main-CtGyoZXd.js";import{ImpactStyle as i,NotificationType as r}from"./index-Bf3KKyiV.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class u extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{u as HapticsWeb};
package/dist/index.html CHANGED
@@ -22,11 +22,11 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-Cjm0d8dZ.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-CtGyoZXd.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
29
- <link rel="stylesheet" crossorigin href="/assets/main-DqC9_fF4.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-C6uuUPJ0.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -8,7 +8,8 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import { agentService, runtimeService, bossMessageService } from '../services/index.js';
11
- import { getClaudeProjectDir } from '../data/index.js';
11
+ import { getClaudeProjectDir, loadAreas, saveAreas } from '../data/index.js';
12
+ import { getAllCustomClasses } from '../services/custom-class-service.js';
12
13
  // Session listing is done inline for performance
13
14
  import { createLogger } from '../utils/logger.js';
14
15
  import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js';
@@ -270,6 +271,213 @@ router.get('/simple', (_req, res) => {
270
271
  const agents = agentService.getAllAgents();
271
272
  res.json(agents.map(agent => ({ id: agent.id, name: agent.name })));
272
273
  });
274
+ // ============================================================================
275
+ // Bulk Operations Routes
276
+ // NOTE: Must be defined BEFORE /:id routes to prevent "bulk" being interpreted as an ID
277
+ // ============================================================================
278
+ // POST /api/agents/bulk/delete - Delete multiple agents by IDs
279
+ router.post('/bulk/delete', async (req, res) => {
280
+ try {
281
+ const { agentIds } = req.body;
282
+ if (!Array.isArray(agentIds) || agentIds.length === 0) {
283
+ res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
284
+ return;
285
+ }
286
+ const deleted = [];
287
+ const failed = [];
288
+ for (const agentId of agentIds) {
289
+ try {
290
+ const agent = agentService.getAgent(agentId);
291
+ if (!agent) {
292
+ failed.push(agentId);
293
+ continue;
294
+ }
295
+ await runtimeService.stopAgent(agentId);
296
+ const success = agentService.deleteAgent(agentId);
297
+ if (success) {
298
+ deleted.push(agentId);
299
+ }
300
+ else {
301
+ failed.push(agentId);
302
+ }
303
+ }
304
+ catch (err) {
305
+ log.error(` Bulk delete failed for agent ${agentId}:`, err);
306
+ failed.push(agentId);
307
+ }
308
+ }
309
+ log.log(`Bulk delete: ${deleted.length} deleted, ${failed.length} failed`);
310
+ res.json({ deleted, failed });
311
+ }
312
+ catch (err) {
313
+ log.error(' Bulk delete failed:', err);
314
+ res.status(500).json({ error: err.message });
315
+ }
316
+ });
317
+ // POST /api/agents/bulk/stop - Stop multiple agents
318
+ router.post('/bulk/stop', async (req, res) => {
319
+ try {
320
+ const { agentIds } = req.body;
321
+ if (!Array.isArray(agentIds) || agentIds.length === 0) {
322
+ res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
323
+ return;
324
+ }
325
+ const stopped = [];
326
+ const failed = [];
327
+ for (const agentId of agentIds) {
328
+ try {
329
+ const agent = agentService.getAgent(agentId);
330
+ if (!agent) {
331
+ failed.push(agentId);
332
+ continue;
333
+ }
334
+ await runtimeService.stopAgent(agentId);
335
+ agentService.updateAgent(agentId, {
336
+ status: 'idle',
337
+ currentTask: undefined,
338
+ currentTool: undefined,
339
+ });
340
+ stopped.push(agentId);
341
+ }
342
+ catch (err) {
343
+ log.error(` Bulk stop failed for agent ${agentId}:`, err);
344
+ failed.push(agentId);
345
+ }
346
+ }
347
+ log.log(`Bulk stop: ${stopped.length} stopped, ${failed.length} failed`);
348
+ res.json({ stopped, failed });
349
+ }
350
+ catch (err) {
351
+ log.error(' Bulk stop failed:', err);
352
+ res.status(500).json({ error: err.message });
353
+ }
354
+ });
355
+ // POST /api/agents/bulk/clear-context - Clear context/reset session for multiple agents
356
+ router.post('/bulk/clear-context', async (req, res) => {
357
+ try {
358
+ const { agentIds } = req.body;
359
+ if (!Array.isArray(agentIds) || agentIds.length === 0) {
360
+ res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
361
+ return;
362
+ }
363
+ const cleared = [];
364
+ const failed = [];
365
+ for (const agentId of agentIds) {
366
+ try {
367
+ const agent = agentService.getAgent(agentId);
368
+ if (!agent) {
369
+ failed.push(agentId);
370
+ continue;
371
+ }
372
+ await runtimeService.stopAgent(agentId);
373
+ agentService.updateAgent(agentId, {
374
+ status: 'idle',
375
+ currentTask: undefined,
376
+ taskLabel: undefined,
377
+ currentTool: undefined,
378
+ lastAssignedTask: undefined,
379
+ lastAssignedTaskTime: undefined,
380
+ sessionId: undefined,
381
+ tokensUsed: 0,
382
+ contextUsed: 0,
383
+ contextStats: undefined,
384
+ });
385
+ cleared.push(agentId);
386
+ }
387
+ catch (err) {
388
+ log.error(` Bulk clear-context failed for agent ${agentId}:`, err);
389
+ failed.push(agentId);
390
+ }
391
+ }
392
+ log.log(`Bulk clear-context: ${cleared.length} cleared, ${failed.length} failed`);
393
+ res.json({ cleared, failed });
394
+ }
395
+ catch (err) {
396
+ log.error(' Bulk clear-context failed:', err);
397
+ res.status(500).json({ error: err.message });
398
+ }
399
+ });
400
+ // POST /api/agents/bulk/move-area - Move multiple agents to an area
401
+ router.post('/bulk/move-area', async (req, res) => {
402
+ try {
403
+ const { agentIds, areaId } = req.body;
404
+ if (!Array.isArray(agentIds) || agentIds.length === 0) {
405
+ res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
406
+ return;
407
+ }
408
+ const areas = loadAreas();
409
+ const moved = [];
410
+ const failed = [];
411
+ for (const agentId of agentIds) {
412
+ try {
413
+ const agent = agentService.getAgent(agentId);
414
+ if (!agent) {
415
+ failed.push(agentId);
416
+ continue;
417
+ }
418
+ // Remove agent from all areas first
419
+ for (const area of areas) {
420
+ area.assignedAgentIds = area.assignedAgentIds.filter(id => id !== agentId);
421
+ }
422
+ // Add to target area if specified
423
+ if (areaId) {
424
+ const targetArea = areas.find(a => a.id === areaId);
425
+ if (!targetArea) {
426
+ failed.push(agentId);
427
+ continue;
428
+ }
429
+ if (!targetArea.assignedAgentIds.includes(agentId)) {
430
+ targetArea.assignedAgentIds.push(agentId);
431
+ }
432
+ }
433
+ moved.push(agentId);
434
+ }
435
+ catch (err) {
436
+ log.error(` Bulk move-area failed for agent ${agentId}:`, err);
437
+ failed.push(agentId);
438
+ }
439
+ }
440
+ // Save areas once after all moves
441
+ if (moved.length > 0) {
442
+ saveAreas(areas);
443
+ }
444
+ log.log(`Bulk move-area: ${moved.length} moved to ${areaId || 'none'}, ${failed.length} failed`);
445
+ res.json({ moved, failed });
446
+ }
447
+ catch (err) {
448
+ log.error(' Bulk move-area failed:', err);
449
+ res.status(500).json({ error: err.message });
450
+ }
451
+ });
452
+ // GET /api/agents/bulk/filters - Return available filter values
453
+ router.get('/bulk/filters', (_req, res) => {
454
+ try {
455
+ const agents = agentService.getAllAgents();
456
+ const areas = loadAreas();
457
+ const customClasses = getAllCustomClasses();
458
+ // Collect unique statuses from agents
459
+ const statuses = [...new Set(agents.map(a => a.status))];
460
+ // Collect unique providers
461
+ const providers = [...new Set(agents.map(a => a.provider))];
462
+ // Collect unique models
463
+ const models = [...new Set(agents.map(a => a.model).filter(Boolean))];
464
+ // Collect all classes (built-in + custom)
465
+ const builtInClasses = ['scout', 'builder', 'debugger', 'architect', 'warrior', 'support', 'boss'];
466
+ const customClassIds = customClasses.map(c => c.id);
467
+ const classes = [...new Set([...builtInClasses, ...customClassIds, ...agents.map(a => a.class)])];
468
+ res.json({
469
+ statuses,
470
+ areas: areas.map(a => ({ id: a.id, name: a.name })),
471
+ providers,
472
+ models,
473
+ classes,
474
+ });
475
+ }
476
+ catch (err) {
477
+ log.error(' Failed to get bulk filters:', err);
478
+ res.status(500).json({ error: err.message });
479
+ }
480
+ });
273
481
  // POST /api/agents - Create new agent
274
482
  router.post('/', async (req, res) => {
275
483
  try {
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as crypto from 'crypto';
9
9
  import { loadAreas, ensureAreaLogosDir, getAreaLogosDir, deleteAreaLogo } from '../data/index.js';
10
+ import { organizeArea, organizeAllAreas } from '../services/area-layout-service.js';
10
11
  import { createLogger } from '../utils/logger.js';
11
12
  const log = createLogger('Areas');
12
13
  const router = Router();
@@ -136,4 +137,28 @@ router.delete('/:areaId/logo', (_req, res) => {
136
137
  res.status(500).json({ error: err.message });
137
138
  }
138
139
  });
140
+ // POST /api/areas/organize-all - Organize agents in all areas
141
+ router.post('/organize-all', (_req, res) => {
142
+ try {
143
+ const results = organizeAllAreas();
144
+ res.json({ results });
145
+ }
146
+ catch (err) {
147
+ log.error(' Failed to organize all areas:', err);
148
+ res.status(500).json({ error: err.message });
149
+ }
150
+ });
151
+ // POST /api/areas/:id/organize - Organize agents within a single area
152
+ router.post('/:areaId/organize', (req, res) => {
153
+ try {
154
+ const { areaId } = req.params;
155
+ const result = organizeArea(String(areaId));
156
+ res.json(result);
157
+ }
158
+ catch (err) {
159
+ log.error(` Failed to organize area ${req.params.areaId}:`, err);
160
+ const status = err.message?.includes('not found') ? 404 : 500;
161
+ res.status(status).json({ error: err.message });
162
+ }
163
+ });
139
164
  export default router;
@@ -16,6 +16,7 @@ import sttRouter from './stt.js';
16
16
  import voiceAssistantRouter from './voice-assistant.js';
17
17
  import snapshotsRouter from './snapshots.js';
18
18
  import areasRouter from './areas.js';
19
+ import workspacesRouter from './workspaces.js';
19
20
  import perfRouter from './perf.js';
20
21
  import triggerRouter, { setBroadcast as setTriggerBroadcast } from './trigger-routes.js';
21
22
  import integrationRouter from './integration-routes.js';
@@ -39,6 +40,7 @@ router.use('/stt', sttRouter);
39
40
  router.use('/voice-assistant', voiceAssistantRouter);
40
41
  router.use('/snapshots', snapshotsRouter);
41
42
  router.use('/areas', areasRouter);
43
+ router.use('/workspaces', workspacesRouter);
42
44
  router.use('/perf', perfRouter);
43
45
  router.use('/triggers', triggerRouter);
44
46
  router.use('/integrations', integrationRouter);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Workspace Routes
3
+ * REST API endpoints for workspace management
4
+ */
5
+ import { Router } from 'express';
6
+ import { getWorkspaces, getWorkspace, createWorkspace, updateWorkspace, deleteWorkspace, getActiveWorkspace, setActiveWorkspace, } from '../services/workspace-service.js';
7
+ import { createLogger } from '../utils/logger.js';
8
+ const log = createLogger('WorkspaceRoutes');
9
+ const router = Router();
10
+ // GET /api/workspaces - List all workspaces
11
+ router.get('/', (_req, res) => {
12
+ try {
13
+ res.json(getWorkspaces());
14
+ }
15
+ catch (err) {
16
+ log.error(`Failed to list workspaces: ${err.message}`);
17
+ res.status(500).json({ error: err.message });
18
+ }
19
+ });
20
+ // GET /api/workspaces/active - Get active workspace ID
21
+ router.get('/active', (_req, res) => {
22
+ try {
23
+ res.json({ workspaceId: getActiveWorkspace() });
24
+ }
25
+ catch (err) {
26
+ log.error(`Failed to get active workspace: ${err.message}`);
27
+ res.status(500).json({ error: err.message });
28
+ }
29
+ });
30
+ // PUT /api/workspaces/active - Set active workspace
31
+ router.put('/active', (req, res) => {
32
+ try {
33
+ const { workspaceId } = req.body;
34
+ if (workspaceId !== null && typeof workspaceId !== 'string') {
35
+ res.status(400).json({ error: 'workspaceId must be a string or null' });
36
+ return;
37
+ }
38
+ setActiveWorkspace(workspaceId ?? null);
39
+ res.json({ workspaceId: getActiveWorkspace() });
40
+ }
41
+ catch (err) {
42
+ log.error(`Failed to set active workspace: ${err.message}`);
43
+ res.status(400).json({ error: err.message });
44
+ }
45
+ });
46
+ // POST /api/workspaces - Create workspace
47
+ router.post('/', (req, res) => {
48
+ try {
49
+ const { name, areaIds } = req.body;
50
+ if (!name || typeof name !== 'string') {
51
+ res.status(400).json({ error: 'name is required and must be a string' });
52
+ return;
53
+ }
54
+ if (!Array.isArray(areaIds)) {
55
+ res.status(400).json({ error: 'areaIds must be an array' });
56
+ return;
57
+ }
58
+ const workspace = createWorkspace(name.trim(), areaIds);
59
+ res.status(201).json(workspace);
60
+ }
61
+ catch (err) {
62
+ log.error(`Failed to create workspace: ${err.message}`);
63
+ res.status(500).json({ error: err.message });
64
+ }
65
+ });
66
+ // PUT /api/workspaces/:id - Update workspace
67
+ router.put('/:id', (req, res) => {
68
+ try {
69
+ const { id } = req.params;
70
+ const { name, areaIds, cameraState, cameraState2d } = req.body;
71
+ const updates = {};
72
+ if (name !== undefined)
73
+ updates.name = name;
74
+ if (areaIds !== undefined)
75
+ updates.areaIds = areaIds;
76
+ if (cameraState !== undefined)
77
+ updates.cameraState = cameraState;
78
+ if (cameraState2d !== undefined)
79
+ updates.cameraState2d = cameraState2d;
80
+ const workspace = updateWorkspace(String(id), updates);
81
+ if (!workspace) {
82
+ res.status(404).json({ error: 'Workspace not found' });
83
+ return;
84
+ }
85
+ res.json(workspace);
86
+ }
87
+ catch (err) {
88
+ log.error(`Failed to update workspace: ${err.message}`);
89
+ res.status(500).json({ error: err.message });
90
+ }
91
+ });
92
+ // DELETE /api/workspaces/:id - Delete workspace
93
+ router.delete('/:id', (req, res) => {
94
+ try {
95
+ const { id } = req.params;
96
+ const deleted = deleteWorkspace(String(id));
97
+ if (!deleted) {
98
+ res.status(404).json({ error: 'Workspace not found' });
99
+ return;
100
+ }
101
+ res.json({ success: true });
102
+ }
103
+ catch (err) {
104
+ log.error(`Failed to delete workspace: ${err.message}`);
105
+ res.status(500).json({ error: err.message });
106
+ }
107
+ });
108
+ // GET /api/workspaces/:id - Get single workspace
109
+ router.get('/:id', (req, res) => {
110
+ try {
111
+ const workspace = getWorkspace(String(req.params.id));
112
+ if (!workspace) {
113
+ res.status(404).json({ error: 'Workspace not found' });
114
+ return;
115
+ }
116
+ res.json(workspace);
117
+ }
118
+ catch (err) {
119
+ log.error(`Failed to get workspace: ${err.message}`);
120
+ res.status(500).json({ error: err.message });
121
+ }
122
+ });
123
+ export default router;
@@ -5,7 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as os from 'os';
7
7
  import * as path from 'path';
8
- import { loadAgents, saveAgents, saveAgentsAsync, getDataDir } from '../data/index.js';
8
+ import { loadAgents, saveAgents, saveAgentsAsync, getDataDir, loadAreas, saveAreas } from '../data/index.js';
9
9
  import { listSessions, getSessionSummary, loadSession, loadToolHistory, searchSession, } from '../claude/session-loader.js';
10
10
  import { loadSubagentHistory } from '../claude/subagent-history-loader.js';
11
11
  import { logger, generateId } from '../utils/index.js';
@@ -309,10 +309,75 @@ export async function createAgent(name, agentClass, cwd, position, sessionId, us
309
309
  // Don't throw - agent is still created in memory
310
310
  }
311
311
  log.log(`✅ Agent ${name} (${id}) created successfully in ${cwd}`);
312
+ // Reconcile area assignment based on initial position
313
+ reconcileAgentAreaAssignment(id, { x: agent.position.x, z: agent.position.z });
312
314
  emit('created', agent);
313
315
  log.log(' Event emitted: created');
314
316
  return agent;
315
317
  }
318
+ /**
319
+ * Check if a point is inside a drawing area.
320
+ */
321
+ function isPositionInArea(pos, area) {
322
+ if (area.archived)
323
+ return false;
324
+ if (area.type === 'rectangle' && area.width && area.height) {
325
+ const halfW = area.width / 2;
326
+ const halfH = area.height / 2;
327
+ return (pos.x >= area.center.x - halfW &&
328
+ pos.x <= area.center.x + halfW &&
329
+ pos.z >= area.center.z - halfH &&
330
+ pos.z <= area.center.z + halfH);
331
+ }
332
+ if (area.type === 'circle' && area.radius) {
333
+ const dx = pos.x - area.center.x;
334
+ const dz = pos.z - area.center.z;
335
+ return dx * dx + dz * dz <= area.radius * area.radius;
336
+ }
337
+ return false;
338
+ }
339
+ /**
340
+ * Reconcile an agent's area assignment based on its physical position.
341
+ * Adds the agent to the area it's inside (if any) and removes it from others.
342
+ */
343
+ function reconcileAgentAreaAssignment(agentId, position) {
344
+ try {
345
+ const areas = loadAreas();
346
+ let changed = false;
347
+ // Find which area the agent is inside (by position)
348
+ let containingAreaId = null;
349
+ for (const area of areas) {
350
+ if (isPositionInArea(position, area)) {
351
+ containingAreaId = area.id;
352
+ break;
353
+ }
354
+ }
355
+ for (const area of areas) {
356
+ const isAssigned = area.assignedAgentIds.includes(agentId);
357
+ if (area.id === containingAreaId) {
358
+ // Agent is inside this area — ensure assigned
359
+ if (!isAssigned) {
360
+ area.assignedAgentIds.push(agentId);
361
+ changed = true;
362
+ }
363
+ }
364
+ else {
365
+ // Agent is NOT inside this area — ensure unassigned
366
+ if (isAssigned) {
367
+ area.assignedAgentIds = area.assignedAgentIds.filter(id => id !== agentId);
368
+ changed = true;
369
+ }
370
+ }
371
+ }
372
+ if (changed) {
373
+ saveAreas(areas);
374
+ }
375
+ }
376
+ catch (err) {
377
+ // Non-critical — don't let area reconciliation break agent updates
378
+ log.error(` Area reconciliation failed for agent ${agentId}:`, err);
379
+ }
380
+ }
316
381
  export function updateAgent(id, updates, updateActivity = true) {
317
382
  const agent = agents.get(id);
318
383
  if (!agent)
@@ -351,6 +416,10 @@ export function updateAgent(id, updates, updateActivity = true) {
351
416
  }
352
417
  agents.set(id, agent);
353
418
  debouncedPersistAgents();
419
+ // Reconcile area assignment when position changes
420
+ if (updates.position) {
421
+ reconcileAgentAreaAssignment(id, { x: agent.position.x, z: agent.position.z });
422
+ }
354
423
  // Debug logging for sessionId changes
355
424
  if (sessionIdBefore !== agent.sessionId) {
356
425
  log.warn(`🔑 [SESSION CHANGE] Agent ${agent.name} (${id}): sessionId changed from "${sessionIdBefore}" to "${agent.sessionId}". Updates had sessionId: ${hasSessionIdInUpdates}, updates keys: ${Object.keys(updates).join(', ')}`);
@@ -364,6 +433,23 @@ export function deleteAgent(id) {
364
433
  return false;
365
434
  agents.delete(id);
366
435
  persistAgents();
436
+ // Clean up area assignments for this agent
437
+ try {
438
+ const areas = loadAreas();
439
+ let changed = false;
440
+ for (const area of areas) {
441
+ const idx = area.assignedAgentIds.indexOf(id);
442
+ if (idx !== -1) {
443
+ area.assignedAgentIds.splice(idx, 1);
444
+ changed = true;
445
+ }
446
+ }
447
+ if (changed)
448
+ saveAreas(areas);
449
+ }
450
+ catch {
451
+ // Non-critical
452
+ }
367
453
  // Clean up skill assignments for this agent (deferred import to avoid circular dependency)
368
454
  setImmediate(async () => {
369
455
  try {