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.
- package/dist/assets/{BossLogsModal-B_dgVF7L.js → BossLogsModal-B6meUM1T.js} +1 -1
- package/dist/assets/BossSpawnModal-DzWs1NNZ.js +1 -0
- package/dist/assets/{ControlsModal-WMTTqbca.js → ControlsModal-D2QDZ20h.js} +1 -1
- package/dist/assets/{DockerLogsModal-THzhLHch.js → DockerLogsModal-Bh1ghgFV.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DWLKJYav.js → EmbeddedEditor-C4rKc9Ys.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-DN9ceaS6.js → GmailOAuthSetup-E5_WFtyX.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-bVST2EOB.js → GoogleOAuthSetup-B7NLxSSp.js} +1 -1
- package/dist/assets/{IframeModal-BELsjvgi.js → IframeModal-CGbINL-o.js} +1 -1
- package/dist/assets/{IntegrationsPanel-DwDr4BRt.js → IntegrationsPanel-DZmbjiXV.js} +2 -2
- package/dist/assets/{LogViewerModal-CMe04PO5.js → LogViewerModal-KI2L074n.js} +1 -1
- package/dist/assets/{MonitoringModal-CqSalNeY.js → MonitoringModal-CWqjvTra.js} +1 -1
- package/dist/assets/{PM2LogsModal-CCmCDxVt.js → PM2LogsModal-DRHybuUO.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-IfzPidIv.js → RestoreArchivedAreaModal-DjpKSz7P.js} +1 -1
- package/dist/assets/{SaveSnapshotModal-DUhrVD5l.js → SaveSnapshotModal-CWtf3rgg.js} +1 -1
- package/dist/assets/Scene2DCanvas-CSfRDtXw.js +1 -0
- package/dist/assets/SceneManager-Do4NpOsy.js +104 -0
- package/dist/assets/{SkillsPanel-CPFOI4Tl.js → SkillsPanel-BqZSNfNW.js} +3 -3
- package/dist/assets/{SnapshotManager-Cbu0tJBz.js → SnapshotManager-DyqyIhWz.js} +1 -1
- package/dist/assets/SpawnModal-CGFj_b9I.js +1 -0
- package/dist/assets/SubordinateAssignmentModal-DGFoupVt.js +1 -0
- package/dist/assets/{SupervisorPanel-BvX-dlk_.js → SupervisorPanel-B1PVopnm.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-RUVFmKmf.js → TriggerManagerPanel-CFez7_vm.js} +2 -2
- package/dist/assets/{WorkflowEditorPanel-CwZpEqzM.js → WorkflowEditorPanel-ClMl5U8g.js} +1 -1
- package/dist/assets/{index-DDPUtz8-.js → index-B4SUGrVB.js} +1 -1
- package/dist/assets/{index-CiD1Rwaq.js → index-BK3kI8Cb.js} +1 -1
- package/dist/assets/{index-BFguOWBW.js → index-Bf3KKyiV.js} +2 -2
- package/dist/assets/{index-D4nfDvz4.js → index-Bmu_CPsU.js} +3 -3
- package/dist/assets/index-CbzHGOPP.js +1 -0
- package/dist/assets/index-CpZt7iSV.js +1 -0
- package/dist/assets/{index-B-wV06cR.js → index-CxtzptPF.js} +5 -5
- package/dist/assets/{index-EH8IBvSU.js → index-D2_-BBP8.js} +3 -3
- package/dist/assets/main-C6uuUPJ0.css +1 -0
- package/dist/assets/main-CtGyoZXd.js +152 -0
- package/dist/assets/{web-D1vWYL8u.js → web-C2JUUvGB.js} +1 -1
- package/dist/assets/{web-DUq3Undh.js → web-D4EcUHSn.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/routes/agents.js +209 -1
- package/dist/src/packages/server/routes/areas.js +25 -0
- package/dist/src/packages/server/routes/index.js +2 -0
- package/dist/src/packages/server/routes/workspaces.js +123 -0
- package/dist/src/packages/server/services/agent-service.js +87 -1
- package/dist/src/packages/server/services/area-layout-service.js +260 -0
- package/dist/src/packages/server/services/workspace-service.js +104 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-CSO1bYxA.js +0 -1
- package/dist/assets/Scene2DCanvas-Bl5DUC7w.js +0 -1
- package/dist/assets/SceneManager-BGO9tiaI.js +0 -104
- package/dist/assets/SpawnModal-BqDbsYLY.js +0 -1
- package/dist/assets/SubordinateAssignmentModal-DOqkhL_L.js +0 -1
- package/dist/assets/camera-D_KeL_pz.js +0 -1
- package/dist/assets/index-C7gqY2AA.js +0 -1
- package/dist/assets/index-H0PzHVFw.js +0 -1
- package/dist/assets/main-Cjm0d8dZ.js +0 -152
- package/dist/assets/main-DqC9_fF4.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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{
|
|
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-
|
|
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-
|
|
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 {
|