groove-dev 0.27.124 → 0.27.126
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +122 -0
- package/node_modules/@groove-dev/daemon/src/preview.js +28 -5
- package/node_modules/@groove-dev/daemon/src/process.js +21 -0
- package/node_modules/@groove-dev/daemon/src/providers/local.js +19 -20
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +66 -3
- package/node_modules/@groove-dev/gui/dist/assets/index-Do3uUrEW.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcmoHTm0.js → index-oPlKeRNb.js} +1749 -1749
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +169 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +8 -10
- package/node_modules/@groove-dev/gui/src/views/models.jsx +580 -236
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +122 -0
- package/packages/daemon/src/preview.js +28 -5
- package/packages/daemon/src/process.js +21 -0
- package/packages/daemon/src/providers/local.js +19 -20
- package/packages/daemon/src/providers/ollama.js +66 -3
- package/packages/gui/dist/assets/index-Do3uUrEW.css +1 -0
- package/packages/gui/dist/assets/{index-BcmoHTm0.js → index-oPlKeRNb.js} +1749 -1749
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/packages/gui/src/stores/groove.js +169 -0
- package/packages/gui/src/views/agents.jsx +8 -10
- package/packages/gui/src/views/models.jsx +580 -236
- package/node_modules/@groove-dev/gui/dist/assets/index-DWI-g_Sm.css +0 -1
- package/packages/gui/dist/assets/index-DWI-g_Sm.css +0 -1
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-oPlKeRNb.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Do3uUrEW.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -83,7 +83,10 @@ export function SpawnWizard() {
|
|
|
83
83
|
const [recommendations, setRecommendations] = useState([]);
|
|
84
84
|
const [preflightDialog, setPreflightDialog] = useState(null);
|
|
85
85
|
const [claudeAuth, setClaudeAuth] = useState(null);
|
|
86
|
+
const [ollamaInstalled, setOllamaInstalled] = useState([]);
|
|
87
|
+
const [ollamaServerRunning, setOllamaServerRunning] = useState(false);
|
|
86
88
|
const federation = useGrooveStore((s) => s.federation);
|
|
89
|
+
const ollamaRunningModels = useGrooveStore((s) => s.ollamaRunningModels);
|
|
87
90
|
|
|
88
91
|
const selectedRole = role || customRole;
|
|
89
92
|
const selectedProvider = providers.find((p) => p.id === provider);
|
|
@@ -92,11 +95,14 @@ export function SpawnWizard() {
|
|
|
92
95
|
|
|
93
96
|
useEffect(() => {
|
|
94
97
|
if (open) {
|
|
98
|
+
const _presetProvider = detailPanel?.presetProvider || '';
|
|
99
|
+
const _presetModel = detailPanel?.presetModel || '';
|
|
100
|
+
|
|
95
101
|
fetchProviders().then((data) => {
|
|
96
102
|
const list = Array.isArray(data) ? data : data.providers || [];
|
|
97
103
|
setProviders(list);
|
|
98
104
|
const installed = list.filter((p) => p.authType === 'api-key' ? (p.installed && p.hasKey) : p.installed);
|
|
99
|
-
if (installed.length > 0 && !
|
|
105
|
+
if (installed.length > 0 && !_presetProvider) {
|
|
100
106
|
const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
|
|
101
107
|
const best = priority.find((pid) => installed.some((p) => p.id === pid)) || installed[0].id;
|
|
102
108
|
setProvider(best);
|
|
@@ -114,7 +120,9 @@ export function SpawnWizard() {
|
|
|
114
120
|
api.get('/personalities').then((data) => {
|
|
115
121
|
setPersonalities(Array.isArray(data) ? data : data.personalities || []);
|
|
116
122
|
}).catch(() => {});
|
|
117
|
-
setRole(''); setCustomRole(''); setName('');
|
|
123
|
+
setRole(''); setCustomRole(''); setName('');
|
|
124
|
+
setProvider(_presetProvider); setModel(_presetModel);
|
|
125
|
+
setPrompt('');
|
|
118
126
|
setSelectedSkills([]);
|
|
119
127
|
setSelectedIntegrations([]);
|
|
120
128
|
setIntegrationApproval('manual');
|
|
@@ -149,6 +157,16 @@ export function SpawnWizard() {
|
|
|
149
157
|
}).catch(() => setClaudeAuth(null));
|
|
150
158
|
}, [open, provider]);
|
|
151
159
|
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!open || provider !== 'ollama') { setOllamaInstalled([]); return; }
|
|
162
|
+
api.get('/providers/ollama/models').then((data) => {
|
|
163
|
+
setOllamaInstalled(data.installed || []);
|
|
164
|
+
}).catch(() => setOllamaInstalled([]));
|
|
165
|
+
api.post('/providers/ollama/check').then((data) => {
|
|
166
|
+
setOllamaServerRunning(data.serverRunning);
|
|
167
|
+
}).catch(() => setOllamaServerRunning(false));
|
|
168
|
+
}, [open, provider]);
|
|
169
|
+
|
|
152
170
|
async function runSpawn() {
|
|
153
171
|
setSpawning(true);
|
|
154
172
|
try {
|
|
@@ -396,9 +414,32 @@ export function SpawnWizard() {
|
|
|
396
414
|
className="w-full h-8 px-3 pr-8 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-40"
|
|
397
415
|
>
|
|
398
416
|
<option value="">Auto</option>
|
|
399
|
-
{
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
{provider === 'ollama' && ollamaInstalled.length > 0 ? (
|
|
418
|
+
<>
|
|
419
|
+
<optgroup label="Installed Models">
|
|
420
|
+
{ollamaInstalled.map((m) => {
|
|
421
|
+
const isRunning = ollamaRunningModels.some((r) => r.name === m.id);
|
|
422
|
+
return (
|
|
423
|
+
<option key={m.id} value={m.id}>
|
|
424
|
+
{m.name || m.id} ({m.size}){isRunning ? ' ● Running' : ''}
|
|
425
|
+
</option>
|
|
426
|
+
);
|
|
427
|
+
})}
|
|
428
|
+
</optgroup>
|
|
429
|
+
<optgroup label="Catalog">
|
|
430
|
+
{availableModels
|
|
431
|
+
.filter((m) => !ollamaInstalled.some((i) => i.id === m.id))
|
|
432
|
+
.map((m) => (
|
|
433
|
+
<option key={m.id} value={m.id}>{m.name} (not installed)</option>
|
|
434
|
+
))
|
|
435
|
+
}
|
|
436
|
+
</optgroup>
|
|
437
|
+
</>
|
|
438
|
+
) : (
|
|
439
|
+
availableModels.map((m) => (
|
|
440
|
+
<option key={m.id} value={m.id}>{m.name}</option>
|
|
441
|
+
))
|
|
442
|
+
)}
|
|
402
443
|
</select>
|
|
403
444
|
<ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none" />
|
|
404
445
|
</div>
|
|
@@ -406,7 +447,7 @@ export function SpawnWizard() {
|
|
|
406
447
|
</div>
|
|
407
448
|
|
|
408
449
|
{provider && selectedProvider && (
|
|
409
|
-
<div className="text-2xs text-text-3 font-sans flex items-center gap-2">
|
|
450
|
+
<div className="text-2xs text-text-3 font-sans flex items-center gap-2 flex-wrap">
|
|
410
451
|
{selectedProvider.authType === 'local' ? (
|
|
411
452
|
<Badge variant="success">Local</Badge>
|
|
412
453
|
) : selectedProvider.authType === 'subscription' ? (
|
|
@@ -419,6 +460,25 @@ export function SpawnWizard() {
|
|
|
419
460
|
</div>
|
|
420
461
|
)}
|
|
421
462
|
|
|
463
|
+
{/* Ollama model status */}
|
|
464
|
+
{provider === 'ollama' && model && (
|
|
465
|
+
<div className="flex items-center gap-2 flex-wrap text-2xs font-sans">
|
|
466
|
+
{ollamaRunningModels.some((r) => r.name === model) ? (
|
|
467
|
+
<Badge variant="success" className="text-2xs gap-1">
|
|
468
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
469
|
+
Ready — running in memory
|
|
470
|
+
</Badge>
|
|
471
|
+
) : ollamaInstalled.some((m) => m.id === model) ? (
|
|
472
|
+
<Badge variant="subtle" className="text-2xs">Will auto-start when agent spawns</Badge>
|
|
473
|
+
) : (
|
|
474
|
+
<Badge variant="warning" className="text-2xs">Not installed — will pull first</Badge>
|
|
475
|
+
)}
|
|
476
|
+
{!ollamaServerRunning && (
|
|
477
|
+
<span className="text-warning">Server not running — will auto-start</span>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
422
482
|
{/* Claude Code Auth */}
|
|
423
483
|
{claudeNotAuthed && (
|
|
424
484
|
<div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3">
|
|
@@ -48,6 +48,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
48
48
|
// ── Providers ────────────────────────────────────────────
|
|
49
49
|
_providerRefreshTick: 0,
|
|
50
50
|
|
|
51
|
+
// ── Local Models (Ollama) ─────────────────────────────────
|
|
52
|
+
ollamaStatus: { installed: false, serverRunning: false, hardware: null },
|
|
53
|
+
ollamaInstalledModels: [],
|
|
54
|
+
ollamaRunningModels: [],
|
|
55
|
+
ollamaCatalog: [],
|
|
56
|
+
ollamaPullProgress: {},
|
|
57
|
+
|
|
51
58
|
// ── Federation ────────────────────────────────────────────
|
|
52
59
|
federation: {
|
|
53
60
|
peers: [],
|
|
@@ -572,6 +579,34 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
572
579
|
get().addChatMessage(msg.agentId, 'system', 'Agent is working — message will be delivered when it finishes.');
|
|
573
580
|
break;
|
|
574
581
|
|
|
582
|
+
case 'ollama:pull:progress':
|
|
583
|
+
set({ ollamaPullProgress: { ...get().ollamaPullProgress, [msg.model]: { status: 'pulling', progress: msg.progress } } });
|
|
584
|
+
break;
|
|
585
|
+
|
|
586
|
+
case 'ollama:pull:complete': {
|
|
587
|
+
const pullProg = { ...get().ollamaPullProgress };
|
|
588
|
+
delete pullProg[msg.model];
|
|
589
|
+
set({ ollamaPullProgress: pullProg });
|
|
590
|
+
get().fetchOllamaStatus();
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
case 'ollama:pull:error': {
|
|
595
|
+
const pullProg2 = { ...get().ollamaPullProgress };
|
|
596
|
+
delete pullProg2[msg.model];
|
|
597
|
+
set({ ollamaPullProgress: pullProg2 });
|
|
598
|
+
get().addToast('error', `Model pull failed: ${msg.error}`);
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'ollama:model:loaded':
|
|
603
|
+
get().fetchOllamaStatus();
|
|
604
|
+
break;
|
|
605
|
+
|
|
606
|
+
case 'ollama:model:unloaded':
|
|
607
|
+
get().fetchOllamaStatus();
|
|
608
|
+
break;
|
|
609
|
+
|
|
575
610
|
case 'rotation:start':
|
|
576
611
|
break;
|
|
577
612
|
|
|
@@ -2060,6 +2095,140 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2060
2095
|
return api.get('/providers');
|
|
2061
2096
|
},
|
|
2062
2097
|
|
|
2098
|
+
// ── Local Models (Ollama) ─────────────────────────────────
|
|
2099
|
+
|
|
2100
|
+
async fetchOllamaStatus() {
|
|
2101
|
+
try {
|
|
2102
|
+
const check = await api.post('/providers/ollama/check');
|
|
2103
|
+
const updates = {
|
|
2104
|
+
ollamaStatus: { installed: check.installed, serverRunning: check.serverRunning, hardware: check.hardware },
|
|
2105
|
+
};
|
|
2106
|
+
if (check.installed) {
|
|
2107
|
+
try {
|
|
2108
|
+
const models = await api.get('/providers/ollama/models');
|
|
2109
|
+
updates.ollamaInstalledModels = models.installed || [];
|
|
2110
|
+
updates.ollamaCatalog = models.catalog || [];
|
|
2111
|
+
} catch {}
|
|
2112
|
+
}
|
|
2113
|
+
if (check.serverRunning) {
|
|
2114
|
+
try {
|
|
2115
|
+
const running = await api.get('/providers/ollama/running');
|
|
2116
|
+
updates.ollamaRunningModels = running.models || [];
|
|
2117
|
+
} catch {
|
|
2118
|
+
updates.ollamaRunningModels = [];
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
updates.ollamaRunningModels = [];
|
|
2122
|
+
}
|
|
2123
|
+
set(updates);
|
|
2124
|
+
return updates.ollamaStatus;
|
|
2125
|
+
} catch {
|
|
2126
|
+
return get().ollamaStatus;
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
|
|
2130
|
+
async startOllamaServer() {
|
|
2131
|
+
try {
|
|
2132
|
+
const result = await api.post('/providers/ollama/serve');
|
|
2133
|
+
if (result.ok) {
|
|
2134
|
+
get().addToast('success', 'Ollama server started');
|
|
2135
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
2136
|
+
await get().fetchOllamaStatus();
|
|
2137
|
+
}
|
|
2138
|
+
return result;
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
get().addToast('error', 'Could not start server', err.message);
|
|
2141
|
+
throw err;
|
|
2142
|
+
}
|
|
2143
|
+
},
|
|
2144
|
+
|
|
2145
|
+
async stopOllamaServer() {
|
|
2146
|
+
try {
|
|
2147
|
+
const result = await api.post('/providers/ollama/stop');
|
|
2148
|
+
if (result.ok) {
|
|
2149
|
+
get().addToast('info', 'Ollama server stopped');
|
|
2150
|
+
set((s) => ({
|
|
2151
|
+
ollamaStatus: { ...s.ollamaStatus, serverRunning: false },
|
|
2152
|
+
ollamaRunningModels: [],
|
|
2153
|
+
}));
|
|
2154
|
+
}
|
|
2155
|
+
return result;
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
get().addToast('error', 'Stop failed', err.message);
|
|
2158
|
+
throw err;
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
|
|
2162
|
+
async restartOllamaServer() {
|
|
2163
|
+
try {
|
|
2164
|
+
const result = await api.post('/providers/ollama/restart');
|
|
2165
|
+
if (result.ok) {
|
|
2166
|
+
get().addToast('success', 'Ollama server restarted');
|
|
2167
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
2168
|
+
await get().fetchOllamaStatus();
|
|
2169
|
+
}
|
|
2170
|
+
return result;
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
get().addToast('error', 'Restart failed', err.message);
|
|
2173
|
+
throw err;
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
|
|
2177
|
+
async pullOllamaModel(modelId) {
|
|
2178
|
+
try {
|
|
2179
|
+
set((s) => ({ ollamaPullProgress: { ...s.ollamaPullProgress, [modelId]: { status: 'pulling', progress: '' } } }));
|
|
2180
|
+
await api.post('/providers/ollama/pull', { model: modelId });
|
|
2181
|
+
set((s) => {
|
|
2182
|
+
const progress = { ...s.ollamaPullProgress };
|
|
2183
|
+
delete progress[modelId];
|
|
2184
|
+
return { ollamaPullProgress: progress };
|
|
2185
|
+
});
|
|
2186
|
+
get().addToast('success', `${modelId} ready to use`);
|
|
2187
|
+
get().fetchOllamaStatus();
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
set((s) => {
|
|
2190
|
+
const progress = { ...s.ollamaPullProgress };
|
|
2191
|
+
delete progress[modelId];
|
|
2192
|
+
return { ollamaPullProgress: progress };
|
|
2193
|
+
});
|
|
2194
|
+
get().addToast('error', `Pull failed: ${err.message}`);
|
|
2195
|
+
}
|
|
2196
|
+
},
|
|
2197
|
+
|
|
2198
|
+
async deleteOllamaModel(modelId) {
|
|
2199
|
+
try {
|
|
2200
|
+
await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
|
|
2201
|
+
set((s) => ({ ollamaInstalledModels: s.ollamaInstalledModels.filter((m) => m.id !== modelId) }));
|
|
2202
|
+
get().addToast('success', `Removed ${modelId}`);
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
get().addToast('error', `Delete failed: ${err.message}`);
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
|
|
2208
|
+
async loadOllamaModel(modelId) {
|
|
2209
|
+
try {
|
|
2210
|
+
await api.post('/providers/ollama/load', { model: modelId });
|
|
2211
|
+
get().addToast('success', `${modelId} loaded into memory`);
|
|
2212
|
+
get().fetchOllamaStatus();
|
|
2213
|
+
} catch (err) {
|
|
2214
|
+
get().addToast('error', `Could not load model: ${err.message}`);
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
|
|
2218
|
+
async unloadOllamaModel(modelId) {
|
|
2219
|
+
try {
|
|
2220
|
+
await api.post('/providers/ollama/unload', { model: modelId });
|
|
2221
|
+
set((s) => ({ ollamaRunningModels: s.ollamaRunningModels.filter((m) => m.name !== modelId) }));
|
|
2222
|
+
get().addToast('info', `${modelId} unloaded`);
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
get().addToast('error', `Unload failed: ${err.message}`);
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
|
|
2228
|
+
spawnFromModel(modelId) {
|
|
2229
|
+
get().openDetail({ type: 'spawn', presetProvider: 'ollama', presetModel: modelId });
|
|
2230
|
+
},
|
|
2231
|
+
|
|
2063
2232
|
// ── Onboarding ────────────────────────────────────────────
|
|
2064
2233
|
|
|
2065
2234
|
async fetchOnboardingStatus() {
|
|
@@ -53,14 +53,12 @@ function savePositions(teamId, positions) {
|
|
|
53
53
|
try { localStorage.setItem(key, s); } catch { /* still over — give up silently */ }
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function loadRoleLayout(
|
|
57
|
-
|
|
58
|
-
try { return JSON.parse(localStorage.getItem(`groove:roleLayout:${teamId}`) || '{}'); } catch { return {}; }
|
|
56
|
+
function loadRoleLayout() {
|
|
57
|
+
try { return JSON.parse(localStorage.getItem('groove:roleLayout') || '{}'); } catch { return {}; }
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
function saveRoleLayout(
|
|
62
|
-
|
|
63
|
-
const key = `groove:roleLayout:${teamId}`;
|
|
60
|
+
function saveRoleLayout(layout) {
|
|
61
|
+
const key = 'groove:roleLayout';
|
|
64
62
|
const s = JSON.stringify(layout);
|
|
65
63
|
try { localStorage.setItem(key, s); return; } catch { /* quota */ }
|
|
66
64
|
if (!freeLocalStorage()) return;
|
|
@@ -367,7 +365,7 @@ function AgentTreeInner() {
|
|
|
367
365
|
// Build nodes — positions are stable, data updates flow to node components
|
|
368
366
|
const targetNodes = useMemo(() => {
|
|
369
367
|
const saved = positionsRef.current;
|
|
370
|
-
const roleLayout = loadRoleLayout(
|
|
368
|
+
const roleLayout = loadRoleLayout();
|
|
371
369
|
const runningCount = agents.filter((a) => a.status === 'running').length;
|
|
372
370
|
|
|
373
371
|
const rootPosition = saved[ROOT_ID] || roleLayout[ROOT_ID] || { x: 0, y: 0 };
|
|
@@ -1589,7 +1587,7 @@ export default function AgentsView() {
|
|
|
1589
1587
|
const positions = loadPositions(activeTeamId);
|
|
1590
1588
|
const layout = {};
|
|
1591
1589
|
const roleCounts = new Map();
|
|
1592
|
-
teamAgents.forEach((agent) => {
|
|
1590
|
+
[...teamAgents].sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)).forEach((agent) => {
|
|
1593
1591
|
const key = agent.name || agent.id;
|
|
1594
1592
|
const pos = positions[key];
|
|
1595
1593
|
if (!pos) return;
|
|
@@ -1600,13 +1598,13 @@ export default function AgentsView() {
|
|
|
1600
1598
|
layout[roleKey] = pos;
|
|
1601
1599
|
});
|
|
1602
1600
|
if (positions[ROOT_ID]) layout[ROOT_ID] = positions[ROOT_ID];
|
|
1603
|
-
saveRoleLayout(
|
|
1601
|
+
saveRoleLayout(layout);
|
|
1604
1602
|
addToast('success', 'Layout saved', 'Future spawns will use these positions');
|
|
1605
1603
|
}}
|
|
1606
1604
|
className="absolute bottom-4 left-28 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
|
|
1607
1605
|
>
|
|
1608
1606
|
<LayoutGrid size={14} />
|
|
1609
|
-
{Object.keys(loadRoleLayout(
|
|
1607
|
+
{Object.keys(loadRoleLayout()).length > 0 ? 'Update Layout' : 'Save Layout'}
|
|
1610
1608
|
</button>
|
|
1611
1609
|
)}
|
|
1612
1610
|
{!isLoading && teamAgents.length > 0 && !workspaceMode && (
|