groove-dev 0.27.143 → 0.27.145
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/CLAUDE.md +0 -7
- 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 +1086 -6532
- package/node_modules/@groove-dev/daemon/src/conversations.js +18 -48
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +812 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +1006 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
- 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/{app.jsx → App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +210 -112
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +200 -18
- package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +335 -152
- package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +25 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +459 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +226 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +54 -12
- package/node_modules/@groove-dev/gui/src/views/models.jsx +419 -496
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- package/node_modules/axios/CHANGELOG.md +260 -0
- package/node_modules/axios/README.md +595 -223
- package/node_modules/axios/dist/axios.js +1460 -1090
- package/node_modules/axios/dist/axios.js.map +1 -1
- package/node_modules/axios/dist/axios.min.js +3 -3
- package/node_modules/axios/dist/axios.min.js.map +1 -1
- package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
- package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
- package/node_modules/axios/dist/esm/axios.js +1557 -1128
- package/node_modules/axios/dist/esm/axios.js.map +1 -1
- package/node_modules/axios/dist/esm/axios.min.js +2 -2
- package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
- package/node_modules/axios/dist/node/axios.cjs +1594 -1057
- package/node_modules/axios/dist/node/axios.cjs.map +1 -1
- package/node_modules/axios/index.d.cts +40 -41
- package/node_modules/axios/index.d.ts +151 -227
- package/node_modules/axios/index.js +2 -0
- package/node_modules/axios/lib/adapters/adapters.js +4 -2
- package/node_modules/axios/lib/adapters/fetch.js +147 -16
- package/node_modules/axios/lib/adapters/http.js +306 -58
- package/node_modules/axios/lib/adapters/xhr.js +6 -2
- package/node_modules/axios/lib/core/Axios.js +7 -3
- package/node_modules/axios/lib/core/AxiosError.js +120 -34
- package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
- package/node_modules/axios/lib/core/buildFullPath.js +1 -1
- package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
- package/node_modules/axios/lib/core/mergeConfig.js +21 -4
- package/node_modules/axios/lib/core/settle.js +7 -11
- package/node_modules/axios/lib/defaults/index.js +14 -9
- package/node_modules/axios/lib/env/data.js +1 -1
- package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
- package/node_modules/axios/lib/helpers/buildURL.js +1 -1
- package/node_modules/axios/lib/helpers/cookies.js +14 -2
- package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
- package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
- package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
- package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
- package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
- package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
- package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
- package/node_modules/axios/lib/helpers/toFormData.js +10 -2
- package/node_modules/axios/lib/helpers/validator.js +3 -1
- package/node_modules/axios/lib/utils.js +33 -21
- package/node_modules/axios/package.json +17 -24
- package/node_modules/follow-redirects/README.md +7 -5
- package/node_modules/follow-redirects/index.js +24 -1
- package/node_modules/follow-redirects/package.json +1 -1
- 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 +1086 -6532
- package/packages/daemon/src/conversations.js +18 -48
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +812 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-Bxc0gU06.js +1006 -0
- package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/{app.jsx → App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +210 -112
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/chat/chat-header.jsx +2 -0
- package/packages/gui/src/components/chat/chat-input.jsx +68 -66
- package/packages/gui/src/components/chat/chat-view.jsx +4 -8
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +200 -18
- package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +335 -152
- package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/components/ui/slider.jsx +8 -8
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +25 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +51 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +459 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +226 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +54 -12
- package/packages/gui/src/views/models.jsx +419 -496
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +0 -984
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -379
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-CCVvAoQn.css +0 -1
- package/packages/gui/dist/assets/index-DGIv_TRm.js +0 -984
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -379
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- package/test.py +0 -571
|
@@ -1,262 +1,57 @@
|
|
|
1
|
-
// GROOVE GUI v2 — Zustand Store
|
|
1
|
+
// GROOVE GUI v2 — Zustand Store (composed from slices)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import { create } from 'zustand';
|
|
5
5
|
import { api } from '../lib/api';
|
|
6
|
+
import { persistJSON } from './helpers.js';
|
|
7
|
+
import { createUiSlice } from './slices/ui-slice.js';
|
|
8
|
+
import { createAgentsSlice } from './slices/agents-slice.js';
|
|
9
|
+
import { createTeamsSlice } from './slices/teams-slice.js';
|
|
10
|
+
import { createChatSlice } from './slices/chat-slice.js';
|
|
11
|
+
import { createEditorSlice } from './slices/editor-slice.js';
|
|
12
|
+
import { createProvidersSlice } from './slices/providers-slice.js';
|
|
13
|
+
import { createNetworkSlice } from './slices/network-slice.js';
|
|
14
|
+
import { createPreviewSlice } from './slices/preview-slice.js';
|
|
15
|
+
import { createMarketplaceSlice } from './slices/marketplace-slice.js';
|
|
16
|
+
import { createAutomationsSlice } from './slices/automations-slice.js';
|
|
6
17
|
|
|
7
18
|
const WS_URL = `ws://${window.location.hostname}:${window.location.port || 31415}`;
|
|
8
19
|
|
|
9
|
-
let toastCounter = 0;
|
|
10
20
|
let plannerPollInterval = null;
|
|
11
|
-
const _modeChangePending = new Set();
|
|
12
|
-
|
|
13
|
-
function loadJSON(key, fallback = {}) {
|
|
14
|
-
try { return JSON.parse(localStorage.getItem(key) || JSON.stringify(fallback)); }
|
|
15
|
-
catch { return fallback; }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function persistJSON(key, value) {
|
|
19
|
-
try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* quota */ }
|
|
20
|
-
}
|
|
21
21
|
|
|
22
22
|
// Clear stale persisted data on version change
|
|
23
23
|
const STORE_VERSION = '0.22.28';
|
|
24
|
-
if (
|
|
24
|
+
if (localStorage.getItem('groove:storeVersion') !== JSON.stringify(STORE_VERSION)) {
|
|
25
25
|
localStorage.removeItem('groove:chatHistory');
|
|
26
26
|
localStorage.removeItem('groove:activityLog');
|
|
27
27
|
persistJSON('groove:storeVersion', STORE_VERSION);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export const useGrooveStore = create((set, get) => ({
|
|
31
|
+
// ── Spread all slices ───────────────────────────────────────
|
|
32
|
+
...createUiSlice(set, get),
|
|
33
|
+
...createAgentsSlice(set, get),
|
|
34
|
+
...createTeamsSlice(set, get),
|
|
35
|
+
...createChatSlice(set, get),
|
|
36
|
+
...createEditorSlice(set, get),
|
|
37
|
+
...createProvidersSlice(set, get),
|
|
38
|
+
...createNetworkSlice(set, get),
|
|
39
|
+
...createPreviewSlice(set, get),
|
|
40
|
+
...createMarketplaceSlice(set, get),
|
|
41
|
+
...createAutomationsSlice(set, get),
|
|
42
|
+
|
|
31
43
|
// ── Connection ────────────────────────────────────────────
|
|
32
|
-
agents: [],
|
|
33
44
|
connected: false,
|
|
34
|
-
hydrated: false,
|
|
45
|
+
hydrated: false,
|
|
35
46
|
ws: null,
|
|
36
47
|
daemonHost: null,
|
|
37
48
|
tunneled: false,
|
|
38
49
|
remoteHomedir: null,
|
|
39
50
|
|
|
40
|
-
// ── Teams ─────────────────────────────────────────────────
|
|
41
|
-
teams: [],
|
|
42
|
-
archivedTeams: [],
|
|
43
|
-
activeTeamId: localStorage.getItem('groove:activeTeamId') || null,
|
|
44
|
-
|
|
45
|
-
// ── Gateways ──────────────────────────────────────────────
|
|
46
|
-
gateways: [],
|
|
47
|
-
|
|
48
|
-
// ── Providers ────────────────────────────────────────────
|
|
49
|
-
_providerRefreshTick: 0,
|
|
50
|
-
|
|
51
|
-
// ── Local Models (Ollama) ─────────────────────────────────
|
|
52
|
-
ollamaStatus: { installed: false, serverRunning: false, hardware: null },
|
|
53
|
-
ollamaInstalledModels: [],
|
|
54
|
-
ollamaRunningModels: [],
|
|
55
|
-
ollamaCatalog: [],
|
|
56
|
-
ollamaPullProgress: {},
|
|
57
|
-
|
|
58
|
-
// ── Federation ────────────────────────────────────────────
|
|
59
|
-
federation: {
|
|
60
|
-
peers: [],
|
|
61
|
-
whitelist: [],
|
|
62
|
-
connections: [],
|
|
63
|
-
pouchLog: [],
|
|
64
|
-
ambassadors: [],
|
|
65
|
-
selectedPeerId: null,
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
// ── Preview ───────────────────────────────────────────────
|
|
69
|
-
previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false },
|
|
70
|
-
showPreviewInAgents: false,
|
|
71
|
-
previewChat: [],
|
|
72
|
-
previewIterating: false,
|
|
73
|
-
teamPreviews: {}, // teamId -> { url, kind, active } — survives team switches
|
|
74
|
-
|
|
75
|
-
// ── Team Launch Config (set during planner spawn, cascades to team) ──
|
|
76
|
-
teamLaunchConfig: null, // { provider, model, reasoningEffort, temperature, verbosity, mode }
|
|
77
|
-
|
|
78
|
-
// ── Team Builder ──────────────────────────────────────────
|
|
79
|
-
teamBuilderOpen: false,
|
|
80
|
-
teamBuilderRoles: [],
|
|
81
|
-
teamBuilderSettings: { provider: null, model: null, reasoningEffort: 50, temperature: 0.5 },
|
|
82
|
-
teamBuilderTask: '',
|
|
83
|
-
teamTemplates: { builtIn: [], custom: [] },
|
|
84
|
-
|
|
85
|
-
// ── Navigation ────────────────────────────────────────────
|
|
86
|
-
activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings' | 'preview'
|
|
87
|
-
detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
|
|
88
|
-
teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
|
|
89
|
-
commandPaletteOpen: false,
|
|
90
|
-
quickConnectOpen: false,
|
|
91
|
-
upgradeModalOpen: false,
|
|
92
|
-
|
|
93
|
-
// ── Node expansion (click-to-open persistent panels) ───────
|
|
94
|
-
expandedNodes: loadJSON('groove:expandedNodes'),
|
|
95
|
-
|
|
96
|
-
// ── Layout persistence ────────────────────────────────────
|
|
97
|
-
detailPanelWidth: Number(localStorage.getItem('groove:detailWidth')) || 480,
|
|
98
|
-
terminalVisible: localStorage.getItem('groove:terminalVisible') === 'true',
|
|
99
|
-
terminalHeight: Number(localStorage.getItem('groove:terminalHeight')) || 260,
|
|
100
|
-
terminalFullHeight: false,
|
|
101
|
-
|
|
102
|
-
// ── Agent data ────────────────────────────────────────────
|
|
103
|
-
activityLog: loadJSON('groove:activityLog'),
|
|
104
|
-
chatHistory: loadJSON('groove:chatHistory'),
|
|
105
|
-
chatInputs: {}, // Per-agent draft input text — persists across tab switches
|
|
106
|
-
tokenTimeline: {},
|
|
107
|
-
|
|
108
|
-
// ── Conversations (Chat view) ────────────────────────────
|
|
109
|
-
conversations: [],
|
|
110
|
-
activeConversationId: localStorage.getItem('groove:activeConversationId') || null,
|
|
111
|
-
conversationMessages: loadJSON('groove:conversationMessages'),
|
|
112
|
-
sendingMessage: false,
|
|
113
|
-
streamingConversationId: null,
|
|
114
|
-
conversationRoles: loadJSON('groove:conversationRoles'),
|
|
115
|
-
conversationReasoningEffort: loadJSON('groove:conversationReasoningEffort'),
|
|
116
|
-
conversationVerbosity: loadJSON('groove:conversationVerbosity'),
|
|
117
|
-
|
|
118
|
-
// ── Keeper (tagged memory) ─────────────────────────────────
|
|
119
|
-
keeperItems: [],
|
|
120
|
-
keeperTree: [],
|
|
121
|
-
keeperEditing: null, // { tag, content, isNew, readOnly } — drives the editor modal
|
|
122
|
-
keeperInstructOpen: false,
|
|
123
|
-
|
|
124
|
-
// ── Approvals ─────────────────────────────────────────────
|
|
125
|
-
pendingApprovals: [],
|
|
126
|
-
resolvedApprovals: [],
|
|
127
|
-
|
|
128
|
-
// ── Recommended Team ──────────────────────────────────────
|
|
129
|
-
recommendedTeam: null, // { name, agents: [...] } from planner
|
|
130
|
-
_delegatingTeamIds: new Set(),
|
|
131
|
-
|
|
132
|
-
// ── Journalist ────────────────────────────────────────────
|
|
133
|
-
journalistStatus: null, // { cycleCount, lastCycleTime, history, lastSynthesis }
|
|
134
|
-
|
|
135
|
-
// ── Network (Early Access) ────────────────────────────────
|
|
136
|
-
networkUnlocked: false,
|
|
137
|
-
networkInstalled: false,
|
|
138
|
-
networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
|
|
139
|
-
networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
|
|
140
|
-
networkStatus: { nodes: [], coverage: 0, totalLayers: 0, models: [], activeSessions: 0 },
|
|
141
|
-
networkStatusReachable: false,
|
|
142
|
-
networkEvents: [],
|
|
143
|
-
networkVersion: { installed: null, latest: null, updateAvailable: false },
|
|
144
|
-
networkUpdateProgress: { updating: false, step: null, message: null, percent: 0, error: null },
|
|
145
|
-
networkCompute: { totalRamMb: 0, totalVramMb: 0, totalCpuCores: 0, totalBandwidthMbps: 0, activeNodes: 0, totalNodes: 0, avgLoad: 0 },
|
|
146
|
-
networkSnapshots: [],
|
|
147
|
-
networkTokenTiming: null,
|
|
148
|
-
networkBenchmarks: [],
|
|
149
|
-
networkTraces: [],
|
|
150
|
-
networkPerfSnapshots: [],
|
|
151
|
-
networkNodeTelemetry: {},
|
|
152
|
-
networkWallet: { connected: false, address: null, balance: '0.00', token: 'GROOVE', chain: 'base-l2' },
|
|
153
|
-
networkEarnings: { today: 0, thisWeek: 0, allTime: 0, history: [] },
|
|
154
|
-
|
|
155
|
-
// ── Training Data ──────────────────────────────────────────
|
|
156
|
-
trainingOptIn: false,
|
|
157
|
-
trainingStats: null,
|
|
158
|
-
dataSharingDismissed: false,
|
|
159
|
-
dataSharingModalOpen: false,
|
|
160
|
-
|
|
161
|
-
// ── Marketplace Auth ───────────────────────────────────────
|
|
162
|
-
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
163
|
-
marketplaceAuthenticated: false,
|
|
164
|
-
edition: 'community', // 'community' | 'pro' — runtime edition from /edition
|
|
165
|
-
subscription: {
|
|
166
|
-
plan: 'community',
|
|
167
|
-
status: 'none',
|
|
168
|
-
active: false,
|
|
169
|
-
features: [],
|
|
170
|
-
seats: 1,
|
|
171
|
-
periodEnd: null,
|
|
172
|
-
cancelAtPeriodEnd: false,
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// ── Version / Auto-Update ──────────────────────────────────
|
|
176
|
-
version: null,
|
|
177
|
-
updateReady: null,
|
|
178
|
-
updateProgress: null,
|
|
179
|
-
updateModalOpen: false,
|
|
180
|
-
|
|
181
|
-
// ── Toasts ────────────────────────────────────────────────
|
|
182
|
-
toasts: [],
|
|
183
|
-
|
|
184
|
-
// ── Project Directory ───────────────────────────────────────
|
|
185
|
-
projectDir: null,
|
|
186
|
-
recentProjects: [],
|
|
187
|
-
showProjectPicker: false,
|
|
188
|
-
|
|
189
|
-
// ── Tunnels ────────────────────────────────────────────────
|
|
190
|
-
savedTunnels: [],
|
|
191
|
-
tunnelConnectStep: null,
|
|
192
|
-
|
|
193
|
-
// ── GitHub Repo Import ────────────────────────────────────
|
|
194
|
-
importedRepos: [],
|
|
195
|
-
importInProgress: false,
|
|
196
|
-
|
|
197
|
-
// ── Editor state ──────────────────────────────────────────
|
|
198
|
-
editorFiles: {},
|
|
199
|
-
editorActiveFile: null,
|
|
200
|
-
editorOpenTabs: [],
|
|
201
|
-
editorTreeCache: {},
|
|
202
|
-
editorChangedFiles: {},
|
|
203
|
-
editorRecentSaves: {},
|
|
204
|
-
editorSidebarWidth: Number(localStorage.getItem('groove:editorSidebarWidth')) || 240,
|
|
205
|
-
editorTheme: localStorage.getItem('groove:editorTheme') || 'vscodeDark',
|
|
206
|
-
|
|
207
|
-
// ── Workspace Mode ────────────────────────────────────────
|
|
208
|
-
workspaceMode: localStorage.getItem('groove:workspaceMode') === 'true',
|
|
209
|
-
workspaceAgentId: null,
|
|
210
|
-
workspaceSnapshots: {},
|
|
211
|
-
workspaceReviewMode: false,
|
|
212
|
-
workspaceReviewFiles: [],
|
|
213
|
-
|
|
214
|
-
// ── Editor (Cursor-style) ────────────────────────────────
|
|
215
|
-
editorSelectedAgent: null,
|
|
216
|
-
editorPendingSnippet: null,
|
|
217
|
-
editorViewMode: 'code',
|
|
218
|
-
editorAiPanelOpen: false,
|
|
219
|
-
editorAiPanelWidth: Number(localStorage.getItem('groove:editorAiPanelWidth')) || 360,
|
|
220
|
-
editorGitStatus: null,
|
|
221
|
-
editorGitBranch: null,
|
|
222
|
-
editorGitDiff: null,
|
|
223
|
-
editorQuickSearchOpen: false,
|
|
224
|
-
|
|
225
|
-
// ── Model Lab ──────────────────────────────────────────────
|
|
226
|
-
labRuntimes: loadJSON('groove:labRuntimes', []),
|
|
227
|
-
labActiveRuntime: null,
|
|
228
|
-
labModels: [],
|
|
229
|
-
labActiveModel: null,
|
|
230
|
-
labPresets: loadJSON('groove:labPresets', []),
|
|
231
|
-
labActivePreset: null,
|
|
232
|
-
labSessions: [],
|
|
233
|
-
labActiveSession: null,
|
|
234
|
-
labMetrics: { ttft: null, tokensPerSec: null, tokensPerSecHistory: [], memory: null, totalTokens: 0, generationTime: null },
|
|
235
|
-
labParameters: loadJSON('groove:labParameters', {
|
|
236
|
-
temperature: 0.7, topP: 0.9, topK: 40, repeatPenalty: 1.1,
|
|
237
|
-
maxTokens: 2048, frequencyPenalty: 0, presencePenalty: 0,
|
|
238
|
-
}),
|
|
239
|
-
labSystemPrompt: localStorage.getItem('groove:labSystemPrompt') || '',
|
|
240
|
-
labStreaming: false,
|
|
241
|
-
labAbortController: null,
|
|
242
|
-
labLocalModels: [],
|
|
243
|
-
labLaunching: null,
|
|
244
|
-
labLlamaInstalled: null,
|
|
245
|
-
labLaunchPhase: null,
|
|
246
|
-
labLaunchError: null,
|
|
247
|
-
labAssistantAgentId: localStorage.getItem('groove:labAssistantAgentId') || null,
|
|
248
|
-
labAssistantMode: false,
|
|
249
|
-
labAssistantBackend: localStorage.getItem('groove:labAssistantBackend') || null,
|
|
250
|
-
|
|
251
|
-
// ── Onboarding ────────────────────────────────────────────
|
|
252
|
-
onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
|
|
253
|
-
|
|
254
|
-
// ── Connection ────────────────────────────────────────────
|
|
255
|
-
|
|
256
51
|
connect() {
|
|
257
52
|
if (get().ws) return;
|
|
258
53
|
const ws = new WebSocket(WS_URL);
|
|
259
|
-
set({ ws });
|
|
54
|
+
set({ ws });
|
|
260
55
|
|
|
261
56
|
ws.onopen = () => {
|
|
262
57
|
set({ connected: true });
|
|
@@ -296,6 +91,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
296
91
|
if (data) set({ subscription: { ...get().subscription, ...data } });
|
|
297
92
|
});
|
|
298
93
|
}
|
|
94
|
+
if (window.groove?.update?.onUpdateAvailable) {
|
|
95
|
+
window.groove.update.onUpdateAvailable((data) => {
|
|
96
|
+
set({ updateProgress: { percent: 0, version: data.version } });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
299
99
|
if (window.groove?.update?.onUpdateProgress) {
|
|
300
100
|
window.groove.update.onUpdateProgress((data) => {
|
|
301
101
|
set({ updateProgress: data });
|
|
@@ -306,6 +106,18 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
306
106
|
set({ updateReady: data.version, updateModalOpen: true, updateProgress: null });
|
|
307
107
|
});
|
|
308
108
|
}
|
|
109
|
+
if (window.groove?.update?.getUpdateStatus) {
|
|
110
|
+
window.groove.update.getUpdateStatus().then((state) => {
|
|
111
|
+
if (!state) return;
|
|
112
|
+
if (state.downloaded) {
|
|
113
|
+
set({ updateReady: state.downloaded.version, updateProgress: null });
|
|
114
|
+
} else if (state.progress) {
|
|
115
|
+
set({ updateProgress: state.progress });
|
|
116
|
+
} else if (state.available) {
|
|
117
|
+
set({ updateProgress: { percent: 0, version: state.available.version } });
|
|
118
|
+
}
|
|
119
|
+
}).catch(() => {});
|
|
120
|
+
}
|
|
309
121
|
};
|
|
310
122
|
|
|
311
123
|
ws.onmessage = (event) => {
|
|
@@ -325,15 +137,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
325
137
|
if (arr.length > 200) timeline[agent.id] = arr.slice(-200);
|
|
326
138
|
}
|
|
327
139
|
}
|
|
328
|
-
// Prune stale tokenTimeline (high-volume metrics, safe to drop).
|
|
329
|
-
// chatHistory and activityLog are NOT pruned here — they must survive
|
|
330
|
-
// the gap between registry.remove() and rotation:complete so the
|
|
331
|
-
// rotation handler can migrate them to the new agent ID. Explicit
|
|
332
|
-
// cleanup happens in killAgent(purge=true) and rotation:complete.
|
|
333
140
|
const st = get();
|
|
334
141
|
for (const id of Object.keys(timeline)) if (!liveIds.has(id)) delete timeline[id];
|
|
335
|
-
// Only replace agents array if something meaningful changed
|
|
336
|
-
// (prevents React Flow tree flicker on every lastActivity update)
|
|
337
142
|
const prev = st.agents;
|
|
338
143
|
const changed = msg.data.length !== prev.length || msg.data.some((a, i) => {
|
|
339
144
|
const p = prev[i];
|
|
@@ -342,7 +147,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
342
147
|
});
|
|
343
148
|
set({ agents: changed ? msg.data : prev, tokenTimeline: timeline, hydrated: true });
|
|
344
149
|
|
|
345
|
-
// Poll for recommended-team.json while a planner is running
|
|
346
150
|
const hasRunningPlanner = msg.data.some((a) => a.role === 'planner' && a.status === 'running');
|
|
347
151
|
if (hasRunningPlanner && !plannerPollInterval && !get().recommendedTeam) {
|
|
348
152
|
plannerPollInterval = setInterval(() => {
|
|
@@ -376,7 +180,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
376
180
|
if (upd) { found++; return upd; }
|
|
377
181
|
return a;
|
|
378
182
|
});
|
|
379
|
-
// New agents not yet in the list
|
|
380
183
|
if (found < changed.length) {
|
|
381
184
|
for (const a of changed) {
|
|
382
185
|
if (!agents.some((ex) => ex.id === a.id)) agents.push(a);
|
|
@@ -402,7 +205,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
402
205
|
case 'agent:output': {
|
|
403
206
|
const { agentId, data } = msg;
|
|
404
207
|
|
|
405
|
-
// Separate text content from tool calls
|
|
406
208
|
let chatText = '';
|
|
407
209
|
let activityText = '';
|
|
408
210
|
if (typeof data.data === 'string') {
|
|
@@ -415,7 +217,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
415
217
|
}).join('\n');
|
|
416
218
|
}
|
|
417
219
|
|
|
418
|
-
// Update agent metrics in real-time (contextUsage, tokensUsed)
|
|
419
220
|
if (data.contextUsage !== undefined || data.tokensUsed !== undefined) {
|
|
420
221
|
const agents = get().agents.map((a) => {
|
|
421
222
|
if (a.id !== agentId) return a;
|
|
@@ -427,17 +228,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
427
228
|
set({ agents });
|
|
428
229
|
}
|
|
429
230
|
|
|
430
|
-
// Text responses → chat bubbles
|
|
431
|
-
// Skip pure token-level stream chunks (subtype='stream') — too granular
|
|
432
|
-
// Show: subtype='assistant' (Claude Code), subtype='text' (agent loop), type='result',
|
|
433
|
-
// and plain activity events with string data (Gemini/Codex/Ollama CLI)
|
|
434
231
|
const isTokenStream = data.subtype === 'stream';
|
|
435
232
|
const showAsChat = chatText && chatText.trim() && !isTokenStream && (
|
|
436
233
|
data.subtype === 'assistant' || data.subtype === 'text' || data.type === 'result' ||
|
|
437
234
|
(data.type === 'activity' && typeof data.data === 'string')
|
|
438
235
|
);
|
|
439
236
|
if (showAsChat) {
|
|
440
|
-
// Clear thinking indicator only when actual text renders as a chat bubble
|
|
441
237
|
if (get().thinkingAgents.has(agentId)) {
|
|
442
238
|
set((s) => {
|
|
443
239
|
const next = new Set(s.thinkingAgents);
|
|
@@ -453,17 +249,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
453
249
|
const last = arr[arr.length - 1];
|
|
454
250
|
const isRecent = last && last.from === 'agent' && (Date.now() - last.timestamp) < 8000;
|
|
455
251
|
|
|
456
|
-
// Skip duplicate text — Claude Code sends 'assistant' then 'result' with same content
|
|
457
252
|
const isDupe = isRecent && (last.text === trimmed || last.text.endsWith(trimmed));
|
|
458
253
|
|
|
459
254
|
if (!isDupe) {
|
|
460
255
|
if (isRecent) {
|
|
461
|
-
// Append to the last agent message (streaming from any provider)
|
|
462
|
-
// Claude Code blocks use \n\n separator; plain text uses space
|
|
463
256
|
const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
|
|
464
257
|
arr[arr.length - 1] = { ...last, text: last.text + sep + trimmed, timestamp: Date.now() };
|
|
465
258
|
} else {
|
|
466
|
-
// New message bubble
|
|
467
259
|
arr.push({ from: 'agent', text: trimmed, timestamp: Date.now() });
|
|
468
260
|
}
|
|
469
261
|
|
|
@@ -472,7 +264,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
472
264
|
persistJSON('groove:chatHistory', history);
|
|
473
265
|
}
|
|
474
266
|
|
|
475
|
-
// Mirror to conversation messages if this agent belongs to a conversation
|
|
476
267
|
const conv = get().conversations.find((c) => c.agentId === agentId);
|
|
477
268
|
if (conv) {
|
|
478
269
|
const convMsgs = { ...get().conversationMessages };
|
|
@@ -495,7 +286,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
495
286
|
}
|
|
496
287
|
}
|
|
497
288
|
|
|
498
|
-
// Tool calls → activity log (shown in streaming bar, not as chat bubbles)
|
|
499
289
|
if (activityText && activityText.trim()) {
|
|
500
290
|
const log = { ...get().activityLog };
|
|
501
291
|
if (!log[agentId]) log[agentId] = [];
|
|
@@ -509,7 +299,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
509
299
|
persistJSON('groove:activityLog', log);
|
|
510
300
|
}
|
|
511
301
|
|
|
512
|
-
// Open-on-write: auto-open files the agent writes in workspace mode
|
|
513
302
|
if (get().workspaceMode && Array.isArray(data.data)) {
|
|
514
303
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'write_file', 'edit_file', 'create_file']);
|
|
515
304
|
for (const block of data.data) {
|
|
@@ -534,7 +323,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
534
323
|
const type = msg.status === 'completed' ? 'success' : isKill ? 'info' : 'warning';
|
|
535
324
|
get().addToast(type, text, msg.error ? msg.error.slice(0, 200) : undefined);
|
|
536
325
|
|
|
537
|
-
// Clear thinking indicator — agent is no longer active
|
|
538
326
|
if (get().thinkingAgents.has(msg.agentId)) {
|
|
539
327
|
set((s) => {
|
|
540
328
|
const next = new Set(s.thinkingAgents);
|
|
@@ -543,17 +331,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
543
331
|
});
|
|
544
332
|
}
|
|
545
333
|
|
|
546
|
-
// Clear conversation streaming state
|
|
547
334
|
const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
|
|
548
335
|
if (exitConv && get().streamingConversationId === exitConv.id) {
|
|
549
336
|
set({ sendingMessage: false, streamingConversationId: null });
|
|
550
337
|
}
|
|
551
338
|
|
|
552
|
-
// Log crash error to agent chat so user can see what happened
|
|
553
339
|
if (msg.error && msg.agentId) {
|
|
554
340
|
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
555
341
|
}
|
|
556
|
-
// Clear workspace if the exiting agent was the workspace target
|
|
557
342
|
if (get().workspaceAgentId === msg.agentId) {
|
|
558
343
|
const teamAgents = get().agents.filter(
|
|
559
344
|
(a) => a.id !== msg.agentId && a.teamId === get().activeTeamId,
|
|
@@ -562,7 +347,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
562
347
|
set({ workspaceAgentId: next?.id || null });
|
|
563
348
|
}
|
|
564
349
|
|
|
565
|
-
// Check for recommended team when planner completes
|
|
566
350
|
if (agent?.role === 'planner' && msg.status === 'completed') {
|
|
567
351
|
setTimeout(() => get().checkRecommendedTeam(), 1000);
|
|
568
352
|
}
|
|
@@ -679,9 +463,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
679
463
|
break;
|
|
680
464
|
|
|
681
465
|
case 'rotation:complete': {
|
|
682
|
-
// Migrate all agent-keyed state to the new ID so chat history,
|
|
683
|
-
// activity log, and token timeline carry forward seamlessly.
|
|
684
|
-
// The broadcast sends `agentId` (new) and `oldAgentId` (old).
|
|
685
466
|
const newId = msg.agentId;
|
|
686
467
|
const oldId = msg.oldAgentId;
|
|
687
468
|
if (!newId || !oldId) break;
|
|
@@ -728,7 +509,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
728
509
|
case 'file:changed': {
|
|
729
510
|
const savedAt = get().editorRecentSaves[msg.path];
|
|
730
511
|
if (savedAt && Date.now() - savedAt < 2000) break;
|
|
731
|
-
// Auto-capture workspace snapshot for diff viewer
|
|
732
512
|
if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
|
|
733
513
|
const existing = get().editorFiles[msg.path];
|
|
734
514
|
if (existing?.content) {
|
|
@@ -779,6 +559,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
779
559
|
|
|
780
560
|
case 'schedule:execute':
|
|
781
561
|
get().addToast('info', `Scheduled agent spawned: ${msg.name || msg.role || 'agent'}`);
|
|
562
|
+
get().fetchAutomations();
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case 'schedule:created':
|
|
566
|
+
case 'schedule:updated':
|
|
567
|
+
case 'schedule:deleted':
|
|
568
|
+
get().fetchAutomations();
|
|
782
569
|
break;
|
|
783
570
|
|
|
784
571
|
case 'gateway:status':
|
|
@@ -957,7 +744,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
957
744
|
}
|
|
958
745
|
set(nsUpdate);
|
|
959
746
|
|
|
960
|
-
// Push snapshot for activity chart
|
|
961
747
|
const wsNodes = nsData.nodes || [];
|
|
962
748
|
const wsOwnId = get().networkNode.nodeId;
|
|
963
749
|
const wsOwn = wsOwnId ? wsNodes.find((n) => (n.node_id || n.nodeId) === wsOwnId) : null;
|
|
@@ -1239,2883 +1025,4 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1239
1025
|
};
|
|
1240
1026
|
ws.onerror = () => ws.close();
|
|
1241
1027
|
},
|
|
1242
|
-
|
|
1243
|
-
// ── Navigation ────────────────────────────────────────────
|
|
1244
|
-
|
|
1245
|
-
setActiveView(view) { set({ activeView: view }); },
|
|
1246
|
-
|
|
1247
|
-
// ── Keeper (tagged memory) ────────────────────────────────
|
|
1248
|
-
|
|
1249
|
-
async fetchKeeperItems() {
|
|
1250
|
-
try {
|
|
1251
|
-
const data = await api.get('/keeper');
|
|
1252
|
-
const treeData = await api.get('/keeper/tree');
|
|
1253
|
-
set({ keeperItems: data.items || [], keeperTree: treeData.tree || [] });
|
|
1254
|
-
} catch { /* ignore */ }
|
|
1255
|
-
},
|
|
1256
|
-
|
|
1257
|
-
async saveKeeperItem(tag, content) {
|
|
1258
|
-
try {
|
|
1259
|
-
const item = await api.post('/keeper', { tag, content });
|
|
1260
|
-
get().fetchKeeperItems();
|
|
1261
|
-
get().addToast('success', `Saved #${item.tag}`);
|
|
1262
|
-
return item;
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
get().addToast('error', 'Failed to save memory', err.message);
|
|
1265
|
-
throw err;
|
|
1266
|
-
}
|
|
1267
|
-
},
|
|
1268
|
-
|
|
1269
|
-
async appendKeeperItem(tag, content) {
|
|
1270
|
-
try {
|
|
1271
|
-
const item = await api.post('/keeper/append', { tag, content });
|
|
1272
|
-
get().fetchKeeperItems();
|
|
1273
|
-
get().addToast('success', `Appended to #${item.tag}`);
|
|
1274
|
-
return item;
|
|
1275
|
-
} catch (err) {
|
|
1276
|
-
get().addToast('error', 'Failed to append', err.message);
|
|
1277
|
-
throw err;
|
|
1278
|
-
}
|
|
1279
|
-
},
|
|
1280
|
-
|
|
1281
|
-
async updateKeeperItem(tag, content) {
|
|
1282
|
-
try {
|
|
1283
|
-
const item = await api.patch(`/keeper/${tag}`, { content });
|
|
1284
|
-
get().fetchKeeperItems();
|
|
1285
|
-
get().addToast('success', `Updated #${item.tag}`);
|
|
1286
|
-
return item;
|
|
1287
|
-
} catch (err) {
|
|
1288
|
-
get().addToast('error', 'Failed to update memory', err.message);
|
|
1289
|
-
throw err;
|
|
1290
|
-
}
|
|
1291
|
-
},
|
|
1292
|
-
|
|
1293
|
-
async deleteKeeperItem(tag) {
|
|
1294
|
-
try {
|
|
1295
|
-
await api.delete(`/keeper/${tag}`);
|
|
1296
|
-
get().fetchKeeperItems();
|
|
1297
|
-
get().addToast('success', `Deleted #${tag}`);
|
|
1298
|
-
} catch (err) {
|
|
1299
|
-
get().addToast('error', 'Failed to delete memory', err.message);
|
|
1300
|
-
}
|
|
1301
|
-
},
|
|
1302
|
-
|
|
1303
|
-
async getKeeperItem(tag) {
|
|
1304
|
-
try {
|
|
1305
|
-
return await api.get(`/keeper/${tag}`);
|
|
1306
|
-
} catch {
|
|
1307
|
-
return null;
|
|
1308
|
-
}
|
|
1309
|
-
},
|
|
1310
|
-
|
|
1311
|
-
async searchKeeper(query) {
|
|
1312
|
-
try {
|
|
1313
|
-
const data = await api.get(`/keeper/search?q=${encodeURIComponent(query)}`);
|
|
1314
|
-
return data.results || [];
|
|
1315
|
-
} catch {
|
|
1316
|
-
return [];
|
|
1317
|
-
}
|
|
1318
|
-
},
|
|
1319
|
-
|
|
1320
|
-
setKeeperEditing(editing) {
|
|
1321
|
-
set({ keeperEditing: editing });
|
|
1322
|
-
},
|
|
1323
|
-
|
|
1324
|
-
async _handleKeeperCommand(agentId, message, command) {
|
|
1325
|
-
const rest = message.replace(/\[\w+[-\w]*\]/i, '').trim();
|
|
1326
|
-
const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
|
|
1327
|
-
|
|
1328
|
-
const addSystemMsg = (text) => {
|
|
1329
|
-
get().addChatMessage(agentId, 'system', text);
|
|
1330
|
-
};
|
|
1331
|
-
|
|
1332
|
-
try {
|
|
1333
|
-
switch (command) {
|
|
1334
|
-
case 'instruct': {
|
|
1335
|
-
set({ keeperInstructOpen: true });
|
|
1336
|
-
return true;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
case 'save': {
|
|
1340
|
-
if (tags.length === 0) { addSystemMsg('Usage: save #tag your message here'); return true; }
|
|
1341
|
-
const content = rest.replace(/#[\w/.-]+/g, '').trim();
|
|
1342
|
-
if (!content) { addSystemMsg('Usage: save #tag your message here'); return true; }
|
|
1343
|
-
await get().saveKeeperItem(tags[0], content);
|
|
1344
|
-
addSystemMsg(`Saved to #${tags[0]}`);
|
|
1345
|
-
return { passthrough: content };
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
case 'append': {
|
|
1349
|
-
if (tags.length === 0) { addSystemMsg('Usage: append #tag content to add'); return true; }
|
|
1350
|
-
const content = rest.replace(/#[\w/.-]+/g, '').trim();
|
|
1351
|
-
if (!content) { addSystemMsg('Usage: append #tag content to add'); return true; }
|
|
1352
|
-
await get().appendKeeperItem(tags[0], content);
|
|
1353
|
-
addSystemMsg(`Appended to #${tags[0]}`);
|
|
1354
|
-
return { passthrough: content };
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
case 'update': {
|
|
1358
|
-
if (tags.length === 0) { addSystemMsg('Usage: [update] #tag'); return true; }
|
|
1359
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1360
|
-
const existing = await get().getKeeperItem(tags[0]);
|
|
1361
|
-
set({ keeperEditing: { tag: tags[0], content: existing?.content || '', isNew: !existing } });
|
|
1362
|
-
return true;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
case 'delete': {
|
|
1366
|
-
if (tags.length === 0) { addSystemMsg('Usage: [delete] #tag'); return true; }
|
|
1367
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1368
|
-
await get().deleteKeeperItem(tags[0]);
|
|
1369
|
-
addSystemMsg(`Deleted #${tags[0]}`);
|
|
1370
|
-
return true;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
case 'view': {
|
|
1374
|
-
if (tags.length === 0) { addSystemMsg('Usage: [view] #tag'); return true; }
|
|
1375
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1376
|
-
const item = await get().getKeeperItem(tags[0]);
|
|
1377
|
-
if (item) {
|
|
1378
|
-
set({ keeperEditing: { tag: tags[0], content: item.content, isNew: false, readOnly: true } });
|
|
1379
|
-
} else {
|
|
1380
|
-
addSystemMsg(`#${tags[0]} not found`);
|
|
1381
|
-
}
|
|
1382
|
-
return true;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
case 'read': {
|
|
1386
|
-
if (tags.length === 0) { addSystemMsg('Usage: [read] #tag1 #tag2 ...'); return true; }
|
|
1387
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1388
|
-
const readBrief = await api.post('/keeper/pull', { tags });
|
|
1389
|
-
if (readBrief?.brief) {
|
|
1390
|
-
await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, {
|
|
1391
|
-
message: `Here is context from my tagged memories:\n\n${readBrief.brief}`,
|
|
1392
|
-
});
|
|
1393
|
-
addSystemMsg(`Sent ${tags.map(t => '#' + t).join(', ')} to agent`);
|
|
1394
|
-
} else {
|
|
1395
|
-
addSystemMsg(`No memories found for ${tags.map(t => '#' + t).join(', ')}`);
|
|
1396
|
-
}
|
|
1397
|
-
return true;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
case 'doc': {
|
|
1401
|
-
if (tags.length === 0) { addSystemMsg('Usage: [doc] #tag'); return true; }
|
|
1402
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1403
|
-
addSystemMsg(`Generating doc for #${tags[0]}...`);
|
|
1404
|
-
const history = get().chatHistory[agentId] || [];
|
|
1405
|
-
const result = await api.post('/keeper/doc', { tag: tags[0], chatHistory: history, agentId });
|
|
1406
|
-
if (result?.content) {
|
|
1407
|
-
addSystemMsg(`Doc #${tags[0]} generated (${result.size}B)`);
|
|
1408
|
-
set({ keeperEditing: { tag: tags[0], content: result.content, isNew: false } });
|
|
1409
|
-
}
|
|
1410
|
-
return true;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
case 'link': {
|
|
1414
|
-
const linkMatch = rest.match(/^((?:#[\w/.-]+\s*)+)\s+(.+)$/);
|
|
1415
|
-
if (!linkMatch || tags.length === 0) { addSystemMsg('Usage: [link] #tag path/to/doc'); return true; }
|
|
1416
|
-
const docPath = linkMatch[2].trim();
|
|
1417
|
-
get().addChatMessage(agentId, 'user', message, false);
|
|
1418
|
-
await api.post('/keeper/link', { tag: tags[0], docPath });
|
|
1419
|
-
addSystemMsg(`Linked #${tags[0]} → ${docPath}`);
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
} catch (err) {
|
|
1424
|
-
addSystemMsg(`Keeper error: ${err.message}`);
|
|
1425
|
-
return true;
|
|
1426
|
-
}
|
|
1427
|
-
return false;
|
|
1428
|
-
},
|
|
1429
|
-
|
|
1430
|
-
// ── Teams ─────────────────────────────────────────────────
|
|
1431
|
-
|
|
1432
|
-
async fetchTeams() {
|
|
1433
|
-
try {
|
|
1434
|
-
const data = await api.get('/teams');
|
|
1435
|
-
let teams = data.teams || [];
|
|
1436
|
-
const defaultTeamId = data.defaultTeamId;
|
|
1437
|
-
try {
|
|
1438
|
-
const saved = JSON.parse(localStorage.getItem('groove:teamOrder') || '[]');
|
|
1439
|
-
if (saved.length) {
|
|
1440
|
-
const byId = Object.fromEntries(teams.map((t) => [t.id, t]));
|
|
1441
|
-
const ordered = saved.filter((id) => byId[id]).map((id) => byId[id]);
|
|
1442
|
-
const remaining = teams.filter((t) => !saved.includes(t.id));
|
|
1443
|
-
teams = [...ordered, ...remaining];
|
|
1444
|
-
}
|
|
1445
|
-
} catch {}
|
|
1446
|
-
const { activeTeamId } = get();
|
|
1447
|
-
const ids = teams.map((t) => t.id);
|
|
1448
|
-
const resolved = ids.includes(activeTeamId) ? activeTeamId : defaultTeamId;
|
|
1449
|
-
set({ teams, activeTeamId: resolved });
|
|
1450
|
-
if (resolved) localStorage.setItem('groove:activeTeamId', resolved);
|
|
1451
|
-
} catch { /* ignore */ }
|
|
1452
|
-
},
|
|
1453
|
-
|
|
1454
|
-
switchTeam(id) {
|
|
1455
|
-
const { activeTeamId, detailPanel, teamDetailPanels, teamPreviews } = get();
|
|
1456
|
-
const updated = { ...teamDetailPanels };
|
|
1457
|
-
if (activeTeamId) updated[activeTeamId] = detailPanel;
|
|
1458
|
-
const restored = updated[id] || null;
|
|
1459
|
-
const tp = teamPreviews[id];
|
|
1460
|
-
const previewUpdate = tp
|
|
1461
|
-
? { previewState: { url: tp.url, teamId: id, kind: tp.kind, deviceSize: 'desktop', screenshotMode: false } }
|
|
1462
|
-
: {};
|
|
1463
|
-
set({ activeTeamId: id, detailPanel: restored, teamDetailPanels: updated, ...previewUpdate });
|
|
1464
|
-
localStorage.setItem('groove:activeTeamId', id);
|
|
1465
|
-
},
|
|
1466
|
-
|
|
1467
|
-
async createTeam(name, workingDir, mode) {
|
|
1468
|
-
try {
|
|
1469
|
-
const body = { name };
|
|
1470
|
-
if (workingDir) body.workingDir = workingDir;
|
|
1471
|
-
if (mode) body.mode = mode;
|
|
1472
|
-
const team = await api.post('/teams', body);
|
|
1473
|
-
// Only set activeTeamId — the WS team:created handler adds to the teams array
|
|
1474
|
-
set({ activeTeamId: team.id });
|
|
1475
|
-
localStorage.setItem('groove:activeTeamId', team.id);
|
|
1476
|
-
get().addToast('success', `Team "${name}" created`);
|
|
1477
|
-
return team;
|
|
1478
|
-
} catch (err) {
|
|
1479
|
-
get().addToast('error', 'Failed to create team', err.message);
|
|
1480
|
-
throw err;
|
|
1481
|
-
}
|
|
1482
|
-
},
|
|
1483
|
-
|
|
1484
|
-
async archiveTeam(id) {
|
|
1485
|
-
const team = get().teams.find((t) => t.id === id);
|
|
1486
|
-
try {
|
|
1487
|
-
await api.delete(`/teams/${encodeURIComponent(id)}`);
|
|
1488
|
-
const wiped = team?.isDefault ? 'wiped' : 'archived';
|
|
1489
|
-
get().addToast('success', `Team "${team?.name}" ${wiped}`, wiped === 'archived' ? 'Files preserved — restore anytime from Archived Teams' : undefined);
|
|
1490
|
-
get().fetchArchivedTeams();
|
|
1491
|
-
} catch (err) {
|
|
1492
|
-
get().addToast('error', 'Failed to archive team', err.message);
|
|
1493
|
-
}
|
|
1494
|
-
},
|
|
1495
|
-
|
|
1496
|
-
async deleteTeamPermanently(id) {
|
|
1497
|
-
const team = get().teams.find((t) => t.id === id);
|
|
1498
|
-
try {
|
|
1499
|
-
await api.delete(`/teams/${encodeURIComponent(id)}?permanent=true`);
|
|
1500
|
-
get().addToast('success', `Team "${team?.name}" permanently deleted`);
|
|
1501
|
-
} catch (err) {
|
|
1502
|
-
get().addToast('error', 'Failed to delete team', err.message);
|
|
1503
|
-
}
|
|
1504
|
-
},
|
|
1505
|
-
|
|
1506
|
-
async deleteTeam(id) {
|
|
1507
|
-
return get().archiveTeam(id);
|
|
1508
|
-
},
|
|
1509
|
-
|
|
1510
|
-
reorderTeams(fromIndex, toIndex) {
|
|
1511
|
-
const teams = [...get().teams];
|
|
1512
|
-
const [moved] = teams.splice(fromIndex, 1);
|
|
1513
|
-
teams.splice(toIndex, 0, moved);
|
|
1514
|
-
set({ teams });
|
|
1515
|
-
try { localStorage.setItem('groove:teamOrder', JSON.stringify(teams.map((t) => t.id))); } catch {}
|
|
1516
|
-
},
|
|
1517
|
-
|
|
1518
|
-
async fetchArchivedTeams() {
|
|
1519
|
-
try {
|
|
1520
|
-
const data = await api.get('/teams/archived');
|
|
1521
|
-
set({ archivedTeams: data.archived || data.teams || [] });
|
|
1522
|
-
} catch { /* endpoint may not exist yet */ }
|
|
1523
|
-
},
|
|
1524
|
-
|
|
1525
|
-
async restoreTeam(archivedId) {
|
|
1526
|
-
try {
|
|
1527
|
-
await api.post(`/teams/archived/${encodeURIComponent(archivedId)}/restore`);
|
|
1528
|
-
get().addToast('success', 'Team restored');
|
|
1529
|
-
get().fetchArchivedTeams();
|
|
1530
|
-
} catch (err) {
|
|
1531
|
-
get().addToast('error', 'Failed to restore team', err.message);
|
|
1532
|
-
}
|
|
1533
|
-
},
|
|
1534
|
-
|
|
1535
|
-
async purgeTeam(archivedId) {
|
|
1536
|
-
try {
|
|
1537
|
-
await api.delete(`/teams/archived/${encodeURIComponent(archivedId)}`);
|
|
1538
|
-
get().addToast('info', 'Archived team permanently deleted');
|
|
1539
|
-
get().fetchArchivedTeams();
|
|
1540
|
-
} catch (err) {
|
|
1541
|
-
get().addToast('error', 'Failed to purge team', err.message);
|
|
1542
|
-
}
|
|
1543
|
-
},
|
|
1544
|
-
|
|
1545
|
-
async cloneTeam(id) {
|
|
1546
|
-
const team = get().teams.find((t) => t.id === id);
|
|
1547
|
-
if (!team) return;
|
|
1548
|
-
const sourceAgents = get().agents.filter((a) => a.teamId === id);
|
|
1549
|
-
try {
|
|
1550
|
-
const newTeam = await api.post('/teams', { name: `${team.name} (copy)` });
|
|
1551
|
-
set({ activeTeamId: newTeam.id });
|
|
1552
|
-
localStorage.setItem('groove:activeTeamId', newTeam.id);
|
|
1553
|
-
for (const agent of sourceAgents) {
|
|
1554
|
-
await api.post('/agents', {
|
|
1555
|
-
role: agent.role,
|
|
1556
|
-
name: agent.name,
|
|
1557
|
-
provider: agent.provider,
|
|
1558
|
-
model: agent.model,
|
|
1559
|
-
scope: agent.scope,
|
|
1560
|
-
teamId: newTeam.id,
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
get().addToast('success', `Cloned "${team.name}" with ${sourceAgents.length} agent${sourceAgents.length !== 1 ? 's' : ''}`);
|
|
1564
|
-
return newTeam;
|
|
1565
|
-
} catch (err) {
|
|
1566
|
-
get().addToast('error', 'Failed to clone team', err.message);
|
|
1567
|
-
}
|
|
1568
|
-
},
|
|
1569
|
-
|
|
1570
|
-
async renameTeam(id, name) {
|
|
1571
|
-
try {
|
|
1572
|
-
const team = await api.patch(`/teams/${encodeURIComponent(id)}`, { name });
|
|
1573
|
-
set((s) => ({ teams: s.teams.map((t) => (t.id === id ? team : t)) }));
|
|
1574
|
-
return team;
|
|
1575
|
-
} catch (err) {
|
|
1576
|
-
get().addToast('error', 'Failed to rename team', err.message);
|
|
1577
|
-
throw err;
|
|
1578
|
-
}
|
|
1579
|
-
},
|
|
1580
|
-
|
|
1581
|
-
async promoteTeam(id) {
|
|
1582
|
-
try {
|
|
1583
|
-
const team = await api.post(`/teams/${encodeURIComponent(id)}/promote`);
|
|
1584
|
-
set((s) => ({ teams: s.teams.filter((t) => t.id !== id) }));
|
|
1585
|
-
get().addToast('success', 'Team promoted — files moved to project directory');
|
|
1586
|
-
return team;
|
|
1587
|
-
} catch (err) {
|
|
1588
|
-
get().addToast('error', 'Failed to promote team', err.message);
|
|
1589
|
-
throw err;
|
|
1590
|
-
}
|
|
1591
|
-
},
|
|
1592
|
-
|
|
1593
|
-
openDetail(descriptor) {
|
|
1594
|
-
const tid = get().activeTeamId;
|
|
1595
|
-
set((s) => ({ detailPanel: descriptor, teamDetailPanels: { ...s.teamDetailPanels, [tid]: descriptor } }));
|
|
1596
|
-
},
|
|
1597
|
-
closeDetail() {
|
|
1598
|
-
const tid = get().activeTeamId;
|
|
1599
|
-
set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
|
|
1600
|
-
},
|
|
1601
|
-
selectAgent(id) {
|
|
1602
|
-
const tid = get().activeTeamId;
|
|
1603
|
-
const match = get().agents.find((a) => a.id === id);
|
|
1604
|
-
if (tid && match && match.teamId && match.teamId !== tid) return;
|
|
1605
|
-
const panel = { type: 'agent', agentId: id };
|
|
1606
|
-
set((s) => ({ detailPanel: panel, teamDetailPanels: { ...s.teamDetailPanels, [tid]: panel } }));
|
|
1607
|
-
},
|
|
1608
|
-
clearSelection() {
|
|
1609
|
-
const tid = get().activeTeamId;
|
|
1610
|
-
set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
|
|
1611
|
-
},
|
|
1612
|
-
toggleCommandPalette() { set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })); },
|
|
1613
|
-
toggleQuickConnect() { set((s) => ({ quickConnectOpen: !s.quickConnectOpen })); },
|
|
1614
|
-
setUpgradeModalOpen: (open) => set({ upgradeModalOpen: open }),
|
|
1615
|
-
|
|
1616
|
-
setDetailPanelWidth(w) {
|
|
1617
|
-
set({ detailPanelWidth: w });
|
|
1618
|
-
localStorage.setItem('groove:detailWidth', String(w));
|
|
1619
|
-
},
|
|
1620
|
-
setTerminalVisible(v) {
|
|
1621
|
-
set({ terminalVisible: v });
|
|
1622
|
-
localStorage.setItem('groove:terminalVisible', String(v));
|
|
1623
|
-
},
|
|
1624
|
-
setTerminalHeight(h) {
|
|
1625
|
-
set({ terminalHeight: h });
|
|
1626
|
-
localStorage.setItem('groove:terminalHeight', String(h));
|
|
1627
|
-
},
|
|
1628
|
-
setTerminalFullHeight(v) { set({ terminalFullHeight: v }); },
|
|
1629
|
-
|
|
1630
|
-
toggleNodeExpanded(id) {
|
|
1631
|
-
const expanded = { ...get().expandedNodes };
|
|
1632
|
-
expanded[id] = !expanded[id];
|
|
1633
|
-
if (!expanded[id]) delete expanded[id];
|
|
1634
|
-
set({ expandedNodes: expanded });
|
|
1635
|
-
persistJSON('groove:expandedNodes', expanded);
|
|
1636
|
-
},
|
|
1637
|
-
|
|
1638
|
-
// ── Preview ──────────────────────────────────────────────
|
|
1639
|
-
|
|
1640
|
-
async fetchActivePreviews() {
|
|
1641
|
-
try {
|
|
1642
|
-
const data = await api.get('/preview');
|
|
1643
|
-
const previews = data.previews || [];
|
|
1644
|
-
if (previews.length > 0) {
|
|
1645
|
-
const updates = {};
|
|
1646
|
-
for (const p of previews) {
|
|
1647
|
-
updates[p.teamId] = { url: `/api/preview/${p.teamId}/proxy/`, kind: p.kind, active: true };
|
|
1648
|
-
}
|
|
1649
|
-
const most = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
|
|
1650
|
-
set((s) => ({
|
|
1651
|
-
teamPreviews: { ...s.teamPreviews, ...updates },
|
|
1652
|
-
previewState: { url: `/api/preview/${most.teamId}/proxy/`, teamId: most.teamId, kind: most.kind, deviceSize: 'desktop', screenshotMode: false },
|
|
1653
|
-
showPreviewInAgents: true,
|
|
1654
|
-
}));
|
|
1655
|
-
}
|
|
1656
|
-
} catch {}
|
|
1657
|
-
},
|
|
1658
|
-
|
|
1659
|
-
openPreview(url, teamId, kind) {
|
|
1660
|
-
set((s) => ({
|
|
1661
|
-
previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false },
|
|
1662
|
-
teamPreviews: { ...s.teamPreviews, [teamId]: { url, kind, active: true } },
|
|
1663
|
-
previewChat: [],
|
|
1664
|
-
showPreviewInAgents: true,
|
|
1665
|
-
}));
|
|
1666
|
-
},
|
|
1667
|
-
closePreview() {
|
|
1668
|
-
set({ showPreviewInAgents: false });
|
|
1669
|
-
},
|
|
1670
|
-
stopPreview() {
|
|
1671
|
-
const { previewState } = get();
|
|
1672
|
-
if (previewState.teamId) {
|
|
1673
|
-
api.delete(`/preview/${previewState.teamId}`).catch(() => {});
|
|
1674
|
-
set((s) => ({
|
|
1675
|
-
teamPreviews: {
|
|
1676
|
-
...s.teamPreviews,
|
|
1677
|
-
[previewState.teamId]: { ...s.teamPreviews[previewState.teamId], active: false },
|
|
1678
|
-
},
|
|
1679
|
-
showPreviewInAgents: false,
|
|
1680
|
-
}));
|
|
1681
|
-
}
|
|
1682
|
-
},
|
|
1683
|
-
async relaunchPreview(teamId) {
|
|
1684
|
-
try {
|
|
1685
|
-
const result = await api.post(`/preview/${teamId}/launch`);
|
|
1686
|
-
if (result.launched) {
|
|
1687
|
-
const proxyUrl = `/api/preview/${teamId}/proxy/`;
|
|
1688
|
-
set((s) => ({
|
|
1689
|
-
previewState: { url: proxyUrl, teamId, kind: result.kind, deviceSize: 'desktop', screenshotMode: false },
|
|
1690
|
-
teamPreviews: { ...s.teamPreviews, [teamId]: { url: proxyUrl, kind: result.kind, active: true } },
|
|
1691
|
-
showPreviewInAgents: true,
|
|
1692
|
-
}));
|
|
1693
|
-
} else {
|
|
1694
|
-
get().addToast('warning', 'Preview could not launch', result.reason ? String(result.reason).slice(0, 200) : 'Build or server failed');
|
|
1695
|
-
}
|
|
1696
|
-
} catch (err) {
|
|
1697
|
-
get().addToast('error', 'Failed to launch preview', err.message);
|
|
1698
|
-
}
|
|
1699
|
-
},
|
|
1700
|
-
togglePreviewInAgents() {
|
|
1701
|
-
set((s) => ({ showPreviewInAgents: !s.showPreviewInAgents }));
|
|
1702
|
-
},
|
|
1703
|
-
setPreviewDevice(size) {
|
|
1704
|
-
set((s) => ({ previewState: { ...s.previewState, deviceSize: size } }));
|
|
1705
|
-
},
|
|
1706
|
-
toggleScreenshotMode() {
|
|
1707
|
-
set((s) => ({ previewState: { ...s.previewState, screenshotMode: !s.previewState.screenshotMode } }));
|
|
1708
|
-
},
|
|
1709
|
-
async iteratePreview(message, screenshotBase64) {
|
|
1710
|
-
const { previewState } = get();
|
|
1711
|
-
if (!previewState.teamId) return;
|
|
1712
|
-
|
|
1713
|
-
const userMsg = { role: 'user', content: message, screenshot: screenshotBase64 || null, timestamp: Date.now() };
|
|
1714
|
-
set((s) => ({ previewChat: [...s.previewChat, userMsg], previewIterating: true }));
|
|
1715
|
-
|
|
1716
|
-
try {
|
|
1717
|
-
const body = { message };
|
|
1718
|
-
if (screenshotBase64) body.screenshot = screenshotBase64;
|
|
1719
|
-
const res = await api.post(`/preview/${previewState.teamId}/iterate`, body);
|
|
1720
|
-
const assistantMsg = { role: 'assistant', content: res.response || res.message || 'Changes routed to planner.', timestamp: Date.now() };
|
|
1721
|
-
set((s) => ({ previewChat: [...s.previewChat, assistantMsg], previewIterating: false }));
|
|
1722
|
-
} catch (err) {
|
|
1723
|
-
const errMsg = { role: 'assistant', content: `Failed to iterate: ${err.message}`, timestamp: Date.now() };
|
|
1724
|
-
set((s) => ({ previewChat: [...s.previewChat, errMsg], previewIterating: false }));
|
|
1725
|
-
}
|
|
1726
|
-
},
|
|
1727
|
-
addPreviewChatMessage(role, content, screenshot) {
|
|
1728
|
-
const msg = { role, content, screenshot: screenshot || null, timestamp: Date.now() };
|
|
1729
|
-
set((s) => ({ previewChat: [...s.previewChat, msg] }));
|
|
1730
|
-
},
|
|
1731
|
-
clearPreviewChat() {
|
|
1732
|
-
set({ previewChat: [] });
|
|
1733
|
-
},
|
|
1734
|
-
|
|
1735
|
-
// ── Toasts ────────────────────────────────────────────────
|
|
1736
|
-
|
|
1737
|
-
addToast(type, message, detail, action, options = {}) {
|
|
1738
|
-
const id = ++toastCounter;
|
|
1739
|
-
const persistent = !!options.persistent;
|
|
1740
|
-
const duration = options.duration;
|
|
1741
|
-
const actions = options.actions;
|
|
1742
|
-
set((s) => ({ toasts: [...s.toasts, { id, type, message, detail, action, actions, persistent, duration }] }));
|
|
1743
|
-
},
|
|
1744
|
-
removeToast(id) {
|
|
1745
|
-
set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }));
|
|
1746
|
-
},
|
|
1747
|
-
|
|
1748
|
-
installUpdate() {
|
|
1749
|
-
window.groove?.update?.installUpdate();
|
|
1750
|
-
},
|
|
1751
|
-
setUpdateModalOpen(open) {
|
|
1752
|
-
set({ updateModalOpen: open });
|
|
1753
|
-
},
|
|
1754
|
-
checkForUpdate() {
|
|
1755
|
-
window.groove?.update?.checkForUpdate();
|
|
1756
|
-
},
|
|
1757
|
-
|
|
1758
|
-
// ── Marketplace Auth ────────────────────────────────────────
|
|
1759
|
-
|
|
1760
|
-
async checkMarketplaceAuth() {
|
|
1761
|
-
try {
|
|
1762
|
-
const data = await api.get('/auth/status');
|
|
1763
|
-
set({
|
|
1764
|
-
marketplaceAuthenticated: data.authenticated || false,
|
|
1765
|
-
marketplaceUser: data.user || null,
|
|
1766
|
-
});
|
|
1767
|
-
try {
|
|
1768
|
-
const edition = await api.get('/edition');
|
|
1769
|
-
set({
|
|
1770
|
-
edition: edition.edition || 'community',
|
|
1771
|
-
subscription: {
|
|
1772
|
-
plan: edition.plan || 'community',
|
|
1773
|
-
status: edition.status || (edition.subscriptionActive ? 'active' : 'none'),
|
|
1774
|
-
active: edition.subscriptionActive === true,
|
|
1775
|
-
features: edition.features || [],
|
|
1776
|
-
seats: edition.seats || 1,
|
|
1777
|
-
periodEnd: edition.periodEnd || null,
|
|
1778
|
-
cancelAtPeriodEnd: edition.cancelAtPeriodEnd || false,
|
|
1779
|
-
},
|
|
1780
|
-
});
|
|
1781
|
-
} catch { /* edition endpoint may not exist */ }
|
|
1782
|
-
} catch {
|
|
1783
|
-
set({ marketplaceAuthenticated: false, marketplaceUser: null });
|
|
1784
|
-
}
|
|
1785
|
-
},
|
|
1786
|
-
|
|
1787
|
-
async marketplaceLogin() {
|
|
1788
|
-
try {
|
|
1789
|
-
const data = await api.get('/auth/login-url');
|
|
1790
|
-
if (data.url) window.open(data.url, '_blank');
|
|
1791
|
-
// Poll for auth completion (user logs in via browser)
|
|
1792
|
-
const poll = setInterval(async () => {
|
|
1793
|
-
try {
|
|
1794
|
-
const status = await api.get('/auth/status');
|
|
1795
|
-
if (status.authenticated) {
|
|
1796
|
-
clearInterval(poll);
|
|
1797
|
-
set({ marketplaceAuthenticated: true, marketplaceUser: status.user });
|
|
1798
|
-
get().addToast('success', `Signed in as ${status.user?.displayName || status.user?.id || 'user'}`);
|
|
1799
|
-
try {
|
|
1800
|
-
const edition = await api.get('/edition');
|
|
1801
|
-
set({
|
|
1802
|
-
edition: edition.edition || 'community',
|
|
1803
|
-
subscription: {
|
|
1804
|
-
plan: edition.plan || 'community',
|
|
1805
|
-
status: edition.status || (edition.subscriptionActive ? 'active' : 'none'),
|
|
1806
|
-
active: edition.subscriptionActive === true,
|
|
1807
|
-
features: edition.features || [],
|
|
1808
|
-
seats: edition.seats || 1,
|
|
1809
|
-
periodEnd: edition.periodEnd || null,
|
|
1810
|
-
cancelAtPeriodEnd: edition.cancelAtPeriodEnd || false,
|
|
1811
|
-
},
|
|
1812
|
-
});
|
|
1813
|
-
} catch { /* edition endpoint may not exist */ }
|
|
1814
|
-
setTimeout(async () => {
|
|
1815
|
-
try {
|
|
1816
|
-
const e = await api.get('/edition');
|
|
1817
|
-
set({
|
|
1818
|
-
edition: e.edition || 'community',
|
|
1819
|
-
subscription: {
|
|
1820
|
-
plan: e.plan || 'community',
|
|
1821
|
-
status: e.status || (e.subscriptionActive ? 'active' : 'none'),
|
|
1822
|
-
active: e.subscriptionActive === true,
|
|
1823
|
-
features: e.features || [],
|
|
1824
|
-
seats: e.seats || 1,
|
|
1825
|
-
periodEnd: e.periodEnd || null,
|
|
1826
|
-
cancelAtPeriodEnd: e.cancelAtPeriodEnd || false,
|
|
1827
|
-
},
|
|
1828
|
-
});
|
|
1829
|
-
} catch { /* delayed re-fetch may fail */ }
|
|
1830
|
-
}, 2000);
|
|
1831
|
-
}
|
|
1832
|
-
} catch { /* keep polling */ }
|
|
1833
|
-
}, 2000);
|
|
1834
|
-
// Stop polling after 5 minutes
|
|
1835
|
-
setTimeout(() => clearInterval(poll), 300000);
|
|
1836
|
-
} catch (err) {
|
|
1837
|
-
get().addToast('error', 'Login failed', err.message);
|
|
1838
|
-
}
|
|
1839
|
-
},
|
|
1840
|
-
|
|
1841
|
-
async marketplaceLogout() {
|
|
1842
|
-
try {
|
|
1843
|
-
await api.post('/auth/logout');
|
|
1844
|
-
set({ marketplaceAuthenticated: false, marketplaceUser: null });
|
|
1845
|
-
get().addToast('info', 'Signed out of marketplace');
|
|
1846
|
-
} catch (err) {
|
|
1847
|
-
get().addToast('error', 'Logout failed', err.message);
|
|
1848
|
-
}
|
|
1849
|
-
},
|
|
1850
|
-
|
|
1851
|
-
async marketplaceCheckout(skillId) {
|
|
1852
|
-
try {
|
|
1853
|
-
const data = await api.post('/auth/checkout', { skillId });
|
|
1854
|
-
if (data.url) window.open(data.url, '_blank');
|
|
1855
|
-
return data;
|
|
1856
|
-
} catch (err) {
|
|
1857
|
-
get().addToast('error', 'Checkout failed', err.message);
|
|
1858
|
-
throw err;
|
|
1859
|
-
}
|
|
1860
|
-
},
|
|
1861
|
-
|
|
1862
|
-
// ── Subscription ────────────────────────────────────────────
|
|
1863
|
-
|
|
1864
|
-
async fetchSubscriptionPlans() {
|
|
1865
|
-
return api.get('/subscription/plans');
|
|
1866
|
-
},
|
|
1867
|
-
|
|
1868
|
-
async startCheckout(priceId) {
|
|
1869
|
-
try {
|
|
1870
|
-
const data = await api.post('/subscription/checkout', { priceId });
|
|
1871
|
-
if (data.url) {
|
|
1872
|
-
if (window.groove?.openExternal) {
|
|
1873
|
-
window.groove.openExternal(data.url);
|
|
1874
|
-
} else {
|
|
1875
|
-
window.open(data.url, '_blank');
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
return data;
|
|
1879
|
-
} catch (err) {
|
|
1880
|
-
if (err.status === 401 || err.message?.includes('Not authenticated')) {
|
|
1881
|
-
get().addToast('info', 'Please sign in to subscribe');
|
|
1882
|
-
get().marketplaceLogin();
|
|
1883
|
-
} else if (err.status === 409) {
|
|
1884
|
-
get().addToast('info', 'Already subscribed', 'Use Manage Subscription to switch plans');
|
|
1885
|
-
} else {
|
|
1886
|
-
get().addToast('error', 'Checkout failed', err.message);
|
|
1887
|
-
}
|
|
1888
|
-
throw err;
|
|
1889
|
-
}
|
|
1890
|
-
},
|
|
1891
|
-
|
|
1892
|
-
async openPortal() {
|
|
1893
|
-
try {
|
|
1894
|
-
const data = await api.post('/subscription/portal');
|
|
1895
|
-
if (data.url) {
|
|
1896
|
-
if (window.groove?.openExternal) {
|
|
1897
|
-
window.groove.openExternal(data.url);
|
|
1898
|
-
} else {
|
|
1899
|
-
window.open(data.url, '_blank');
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
return data;
|
|
1903
|
-
} catch (err) {
|
|
1904
|
-
get().addToast('error', 'Portal failed', err.message);
|
|
1905
|
-
throw err;
|
|
1906
|
-
}
|
|
1907
|
-
},
|
|
1908
|
-
|
|
1909
|
-
async updateSeats(seats) {
|
|
1910
|
-
try {
|
|
1911
|
-
const data = await api.patch('/subscription', { seats });
|
|
1912
|
-
set({ subscription: { ...get().subscription, ...data } });
|
|
1913
|
-
get().addToast('success', `Updated to ${seats} seat${seats !== 1 ? 's' : ''}`);
|
|
1914
|
-
return data;
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
get().addToast('error', 'Seat update failed', err.message);
|
|
1917
|
-
throw err;
|
|
1918
|
-
}
|
|
1919
|
-
},
|
|
1920
|
-
|
|
1921
|
-
// ── Approvals ──────────────────────────────────────────────
|
|
1922
|
-
|
|
1923
|
-
async fetchApprovals() {
|
|
1924
|
-
try {
|
|
1925
|
-
const data = await api.get('/approvals');
|
|
1926
|
-
set({
|
|
1927
|
-
pendingApprovals: data.pending || [],
|
|
1928
|
-
resolvedApprovals: data.resolved || [],
|
|
1929
|
-
});
|
|
1930
|
-
} catch { /* ignore */ }
|
|
1931
|
-
},
|
|
1932
|
-
|
|
1933
|
-
async approveRequest(id) {
|
|
1934
|
-
try {
|
|
1935
|
-
await api.post(`/approvals/${encodeURIComponent(id)}/approve`);
|
|
1936
|
-
set((s) => ({ pendingApprovals: s.pendingApprovals.filter((a) => a.id !== id) }));
|
|
1937
|
-
get().addToast('success', 'Approved');
|
|
1938
|
-
} catch (err) {
|
|
1939
|
-
get().addToast('error', 'Approve failed', err.message);
|
|
1940
|
-
}
|
|
1941
|
-
},
|
|
1942
|
-
|
|
1943
|
-
async rejectRequest(id, reason = '') {
|
|
1944
|
-
try {
|
|
1945
|
-
await api.post(`/approvals/${encodeURIComponent(id)}/reject`, { reason });
|
|
1946
|
-
set((s) => ({ pendingApprovals: s.pendingApprovals.filter((a) => a.id !== id) }));
|
|
1947
|
-
get().addToast('info', 'Rejected');
|
|
1948
|
-
} catch (err) {
|
|
1949
|
-
get().addToast('error', 'Reject failed', err.message);
|
|
1950
|
-
}
|
|
1951
|
-
},
|
|
1952
|
-
|
|
1953
|
-
// ── Recommended Team ──────────────────────────────────────
|
|
1954
|
-
|
|
1955
|
-
async checkRecommendedTeam() {
|
|
1956
|
-
try {
|
|
1957
|
-
const data = await api.get('/recommended-team');
|
|
1958
|
-
if (!data || !data.agents?.length) {
|
|
1959
|
-
set({ recommendedTeam: null });
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Check if all recommended roles already exist in the planner's team.
|
|
1964
|
-
// If so, auto-delegate instead of showing the "Launch Team" modal.
|
|
1965
|
-
const teamId = data.teamId || null;
|
|
1966
|
-
|
|
1967
|
-
if (teamId) {
|
|
1968
|
-
const teamAgents = get().agents.filter((a) => a.teamId === teamId && a.role !== 'planner');
|
|
1969
|
-
const phase1Roles = data.agents.filter((a) => !a.phase || a.phase === 1).map((a) => a.role);
|
|
1970
|
-
const allExist = phase1Roles.every((role) => teamAgents.some((a) => a.role === role));
|
|
1971
|
-
|
|
1972
|
-
if (allExist && phase1Roles.length > 0) {
|
|
1973
|
-
// Guard: skip if already delegating for this team (poll race)
|
|
1974
|
-
if (get()._delegatingTeamIds.has(teamId)) return;
|
|
1975
|
-
set((s) => ({ recommendedTeam: null, _delegatingTeamIds: new Set([...s._delegatingTeamIds, teamId]) }));
|
|
1976
|
-
try {
|
|
1977
|
-
const tlc = get().teamLaunchConfig;
|
|
1978
|
-
const result = await api.post('/recommended-team/launch', {
|
|
1979
|
-
teamId,
|
|
1980
|
-
...(tlc?.provider && { teamProvider: tlc.provider }),
|
|
1981
|
-
...(tlc?.model && { teamModel: tlc.model }),
|
|
1982
|
-
...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
|
|
1983
|
-
...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
|
|
1984
|
-
});
|
|
1985
|
-
const agents = result.agents || [];
|
|
1986
|
-
const failures = result.failed || [];
|
|
1987
|
-
const names = agents.map((a) => a.name).join(', ') || '';
|
|
1988
|
-
|
|
1989
|
-
if (agents.length === 0 && failures.length > 0) {
|
|
1990
|
-
get().addToast('error', 'Delegation failed', failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1991
|
-
} else {
|
|
1992
|
-
get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
|
|
1993
|
-
if (failures.length > 0) {
|
|
1994
|
-
get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
if (agents.length > 0) {
|
|
1998
|
-
set((s) => ({
|
|
1999
|
-
thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
|
|
2000
|
-
}));
|
|
2001
|
-
}
|
|
2002
|
-
} finally {
|
|
2003
|
-
set((s) => {
|
|
2004
|
-
const next = new Set(s._delegatingTeamIds);
|
|
2005
|
-
next.delete(teamId);
|
|
2006
|
-
return { _delegatingTeamIds: next };
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
// New agents needed — show the modal for approval
|
|
2014
|
-
set({ recommendedTeam: { ...data, teamId: data.teamId || null } });
|
|
2015
|
-
} catch {
|
|
2016
|
-
set({ recommendedTeam: null });
|
|
2017
|
-
}
|
|
2018
|
-
},
|
|
2019
|
-
|
|
2020
|
-
async launchRecommendedTeam(modifiedAgents) {
|
|
2021
|
-
try {
|
|
2022
|
-
const teamId = get().recommendedTeam?.teamId || null;
|
|
2023
|
-
const tlc = get().teamLaunchConfig;
|
|
2024
|
-
set({ recommendedTeam: null }); // Dismiss modal immediately
|
|
2025
|
-
get().addToast('info', 'Launching team...');
|
|
2026
|
-
const body = {
|
|
2027
|
-
...(modifiedAgents && { agents: modifiedAgents }),
|
|
2028
|
-
...(teamId && { teamId }),
|
|
2029
|
-
...(tlc?.provider && { teamProvider: tlc.provider }),
|
|
2030
|
-
...(tlc?.model && { teamModel: tlc.model }),
|
|
2031
|
-
...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
|
|
2032
|
-
...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
|
|
2033
|
-
...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
|
|
2034
|
-
};
|
|
2035
|
-
const result = await api.post('/recommended-team/launch', body);
|
|
2036
|
-
const totalOk = (result.launched || 0) + (result.reused || 0);
|
|
2037
|
-
const failures = result.failed || [];
|
|
2038
|
-
|
|
2039
|
-
if (totalOk === 0 && failures.length > 0) {
|
|
2040
|
-
get().addToast('error', 'Team launch failed', failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
2041
|
-
} else {
|
|
2042
|
-
const sub = [
|
|
2043
|
-
result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
|
|
2044
|
-
result.projectDir ? `→ ${result.projectDir}/` : '',
|
|
2045
|
-
].filter(Boolean).join(' · ');
|
|
2046
|
-
get().addToast('success', `Launched ${totalOk} agents`, sub || undefined);
|
|
2047
|
-
if (failures.length > 0) {
|
|
2048
|
-
get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
// Set thinking indicator for all launched/reused agents
|
|
2052
|
-
const launchedAgents = result.agents || [];
|
|
2053
|
-
if (launchedAgents.length > 0) {
|
|
2054
|
-
set((s) => ({
|
|
2055
|
-
thinkingAgents: new Set([...s.thinkingAgents, ...launchedAgents.map((a) => a.id)]),
|
|
2056
|
-
}));
|
|
2057
|
-
}
|
|
2058
|
-
// Clean up stale files — scoped to the launched team so plans in other
|
|
2059
|
-
// teams' workspaces survive. The launch endpoint already unlinks the
|
|
2060
|
-
// exact plan it read; this is a belt-and-suspenders sweep.
|
|
2061
|
-
const launchedTeamId = body?.teamId || result?.teamId || null;
|
|
2062
|
-
if (launchedTeamId) {
|
|
2063
|
-
api.post('/cleanup', { teamId: launchedTeamId }).catch(() => {});
|
|
2064
|
-
}
|
|
2065
|
-
return result;
|
|
2066
|
-
} catch (err) {
|
|
2067
|
-
get().addToast('error', 'Launch failed', err.message);
|
|
2068
|
-
throw err;
|
|
2069
|
-
}
|
|
2070
|
-
},
|
|
2071
|
-
|
|
2072
|
-
// ── Team Builder ──────────────────────────────────────────
|
|
2073
|
-
|
|
2074
|
-
openTeamBuilder() { set({ teamBuilderOpen: true }); },
|
|
2075
|
-
closeTeamBuilder() {
|
|
2076
|
-
set({
|
|
2077
|
-
teamBuilderOpen: false,
|
|
2078
|
-
teamBuilderRoles: [],
|
|
2079
|
-
teamBuilderSettings: { provider: null, model: null, reasoningEffort: 50, temperature: 0.5 },
|
|
2080
|
-
teamBuilderTask: '',
|
|
2081
|
-
});
|
|
2082
|
-
},
|
|
2083
|
-
addTeamBuilderRole(role) {
|
|
2084
|
-
set((s) => ({
|
|
2085
|
-
teamBuilderRoles: [...s.teamBuilderRoles, {
|
|
2086
|
-
role, name: '', provider: null, model: null,
|
|
2087
|
-
reasoningEffort: null, temperature: null, prompt: '',
|
|
2088
|
-
}],
|
|
2089
|
-
}));
|
|
2090
|
-
},
|
|
2091
|
-
removeTeamBuilderRole(index) {
|
|
2092
|
-
set((s) => ({ teamBuilderRoles: s.teamBuilderRoles.filter((_, i) => i !== index) }));
|
|
2093
|
-
},
|
|
2094
|
-
updateTeamBuilderRole(index, updates) {
|
|
2095
|
-
set((s) => ({
|
|
2096
|
-
teamBuilderRoles: s.teamBuilderRoles.map((r, i) => i === index ? { ...r, ...updates } : r),
|
|
2097
|
-
}));
|
|
2098
|
-
},
|
|
2099
|
-
applyTemplate(template) {
|
|
2100
|
-
set({
|
|
2101
|
-
teamBuilderRoles: (template.roles || []).map((r) => ({
|
|
2102
|
-
role: typeof r === 'string' ? r : r.role,
|
|
2103
|
-
name: '', provider: null, model: null,
|
|
2104
|
-
reasoningEffort: null, temperature: null, prompt: '',
|
|
2105
|
-
})),
|
|
2106
|
-
});
|
|
2107
|
-
},
|
|
2108
|
-
setTeamBuilderSettings(settings) {
|
|
2109
|
-
set((s) => ({ teamBuilderSettings: { ...s.teamBuilderSettings, ...settings } }));
|
|
2110
|
-
},
|
|
2111
|
-
setTeamBuilderTask(task) { set({ teamBuilderTask: task }); },
|
|
2112
|
-
|
|
2113
|
-
async fetchTeamTemplates() {
|
|
2114
|
-
try {
|
|
2115
|
-
const data = await api.get('/team-templates');
|
|
2116
|
-
const builtIn = [];
|
|
2117
|
-
const custom = [];
|
|
2118
|
-
for (const [key, tmpl] of Object.entries(data || {})) {
|
|
2119
|
-
const entry = { ...tmpl, name: key };
|
|
2120
|
-
if (tmpl.builtIn) builtIn.push(entry);
|
|
2121
|
-
else custom.push(entry);
|
|
2122
|
-
}
|
|
2123
|
-
set({ teamTemplates: { builtIn, custom } });
|
|
2124
|
-
} catch { /* endpoint may not exist yet */ }
|
|
2125
|
-
},
|
|
2126
|
-
|
|
2127
|
-
async saveTeamTemplate(name) {
|
|
2128
|
-
try {
|
|
2129
|
-
const { teamBuilderRoles, teamBuilderSettings } = get();
|
|
2130
|
-
await api.post('/team-templates', {
|
|
2131
|
-
name,
|
|
2132
|
-
roles: teamBuilderRoles.map((r) => r.role),
|
|
2133
|
-
settings: teamBuilderSettings,
|
|
2134
|
-
});
|
|
2135
|
-
get().addToast('success', `Template "${name}" saved`);
|
|
2136
|
-
get().fetchTeamTemplates();
|
|
2137
|
-
} catch (err) {
|
|
2138
|
-
get().addToast('error', 'Failed to save template', err.message);
|
|
2139
|
-
}
|
|
2140
|
-
},
|
|
2141
|
-
|
|
2142
|
-
async deleteTeamTemplate(name) {
|
|
2143
|
-
try {
|
|
2144
|
-
await api.delete(`/team-templates/${encodeURIComponent(name)}`);
|
|
2145
|
-
get().addToast('info', `Template "${name}" deleted`);
|
|
2146
|
-
get().fetchTeamTemplates();
|
|
2147
|
-
} catch (err) {
|
|
2148
|
-
get().addToast('error', 'Failed to delete template', err.message);
|
|
2149
|
-
}
|
|
2150
|
-
},
|
|
2151
|
-
|
|
2152
|
-
async launchTeamBuilder() {
|
|
2153
|
-
const { teamBuilderRoles, teamBuilderSettings, teamBuilderTask, activeTeamId } = get();
|
|
2154
|
-
if (teamBuilderRoles.length === 0) return;
|
|
2155
|
-
set({ teamLaunchConfig: {
|
|
2156
|
-
provider: teamBuilderSettings.provider || null,
|
|
2157
|
-
model: teamBuilderSettings.model || null,
|
|
2158
|
-
reasoningEffort: teamBuilderSettings.reasoningEffort,
|
|
2159
|
-
temperature: teamBuilderSettings.temperature,
|
|
2160
|
-
}});
|
|
2161
|
-
get().closeTeamBuilder();
|
|
2162
|
-
try {
|
|
2163
|
-
const body = {
|
|
2164
|
-
task: teamBuilderTask,
|
|
2165
|
-
roles: teamBuilderRoles,
|
|
2166
|
-
settings: teamBuilderSettings,
|
|
2167
|
-
launchMode: 'plan-first',
|
|
2168
|
-
teamId: activeTeamId,
|
|
2169
|
-
};
|
|
2170
|
-
const result = await api.post('/team-builder/launch', body);
|
|
2171
|
-
get().addToast('success', 'Planner spawned — team will build automatically');
|
|
2172
|
-
return result;
|
|
2173
|
-
} catch (err) {
|
|
2174
|
-
get().addToast('error', 'Team launch failed', err.message);
|
|
2175
|
-
throw err;
|
|
2176
|
-
}
|
|
2177
|
-
},
|
|
2178
|
-
|
|
2179
|
-
// ── GitHub Repo Import ────────────────────────────────────
|
|
2180
|
-
|
|
2181
|
-
async fetchImportedRepos() {
|
|
2182
|
-
try {
|
|
2183
|
-
const repos = await api.get('/repos/imported');
|
|
2184
|
-
set({ importedRepos: repos });
|
|
2185
|
-
} catch { /* ignore */ }
|
|
2186
|
-
},
|
|
2187
|
-
|
|
2188
|
-
async previewRepo(repoUrl) {
|
|
2189
|
-
return api.post('/repos/preview', { repoUrl });
|
|
2190
|
-
},
|
|
2191
|
-
|
|
2192
|
-
async importRepo(repoUrl, targetPath, createTeam, teamName) {
|
|
2193
|
-
set({ importInProgress: true });
|
|
2194
|
-
try {
|
|
2195
|
-
const result = await api.post('/repos/import', { repoUrl, targetPath, createTeam, teamName });
|
|
2196
|
-
get().fetchImportedRepos();
|
|
2197
|
-
return result;
|
|
2198
|
-
} finally {
|
|
2199
|
-
set({ importInProgress: false });
|
|
2200
|
-
}
|
|
2201
|
-
},
|
|
2202
|
-
|
|
2203
|
-
async softRemoveRepo(importId) {
|
|
2204
|
-
await api.delete(`/repos/${encodeURIComponent(importId)}/remove`);
|
|
2205
|
-
get().fetchImportedRepos();
|
|
2206
|
-
},
|
|
2207
|
-
|
|
2208
|
-
async hardNukeRepo(importId, deleteFiles = true) {
|
|
2209
|
-
await api.delete(`/repos/${encodeURIComponent(importId)}/nuke?deleteFiles=${deleteFiles}`);
|
|
2210
|
-
get().fetchImportedRepos();
|
|
2211
|
-
},
|
|
2212
|
-
|
|
2213
|
-
// ── Project Directory ────────────────────────────────────
|
|
2214
|
-
|
|
2215
|
-
async fetchProjectDir() {
|
|
2216
|
-
try {
|
|
2217
|
-
const data = await api.get('/project-dir');
|
|
2218
|
-
const isHome = /^\/home\/[^/]+$/.test(data.projectDir) || data.projectDir === '/root';
|
|
2219
|
-
set({
|
|
2220
|
-
projectDir: data.projectDir,
|
|
2221
|
-
recentProjects: data.recentProjects || [],
|
|
2222
|
-
showProjectPicker: isHome || (data.recentProjects || []).length === 0,
|
|
2223
|
-
editorTreeCache: {},
|
|
2224
|
-
});
|
|
2225
|
-
} catch {}
|
|
2226
|
-
},
|
|
2227
|
-
|
|
2228
|
-
async setProjectDir(path) {
|
|
2229
|
-
const data = await api.post('/project-dir', { path });
|
|
2230
|
-
try { await api.post('/files/root', { root: data.projectDir }); } catch {}
|
|
2231
|
-
set({
|
|
2232
|
-
projectDir: data.projectDir,
|
|
2233
|
-
recentProjects: data.recentProjects || [],
|
|
2234
|
-
showProjectPicker: false,
|
|
2235
|
-
editorTreeCache: {},
|
|
2236
|
-
});
|
|
2237
|
-
get().fetchTreeDir('');
|
|
2238
|
-
},
|
|
2239
|
-
|
|
2240
|
-
async removeRecentProject(path) {
|
|
2241
|
-
try {
|
|
2242
|
-
await api.delete('/projects/recent', { path });
|
|
2243
|
-
} catch {}
|
|
2244
|
-
get().fetchProjectDir();
|
|
2245
|
-
},
|
|
2246
|
-
|
|
2247
|
-
toggleProjectPicker() {
|
|
2248
|
-
set((s) => ({ showProjectPicker: !s.showProjectPicker }));
|
|
2249
|
-
},
|
|
2250
|
-
|
|
2251
|
-
// ── Tunnels ──────────────────────────────────────────────
|
|
2252
|
-
|
|
2253
|
-
async fetchTunnels() {
|
|
2254
|
-
try {
|
|
2255
|
-
const tunnels = await api.get('/tunnels');
|
|
2256
|
-
set({ savedTunnels: Array.isArray(tunnels) ? tunnels : [] });
|
|
2257
|
-
} catch {}
|
|
2258
|
-
},
|
|
2259
|
-
|
|
2260
|
-
async saveTunnel(config) {
|
|
2261
|
-
const result = await api.post('/tunnels', config);
|
|
2262
|
-
get().fetchTunnels();
|
|
2263
|
-
return result;
|
|
2264
|
-
},
|
|
2265
|
-
|
|
2266
|
-
async updateTunnel(id, config) {
|
|
2267
|
-
const result = await api.patch(`/tunnels/${encodeURIComponent(id)}`, config);
|
|
2268
|
-
get().fetchTunnels();
|
|
2269
|
-
return result;
|
|
2270
|
-
},
|
|
2271
|
-
|
|
2272
|
-
async deleteTunnel(id) {
|
|
2273
|
-
await api.delete(`/tunnels/${encodeURIComponent(id)}`);
|
|
2274
|
-
get().fetchTunnels();
|
|
2275
|
-
},
|
|
2276
|
-
|
|
2277
|
-
async testTunnel(id) {
|
|
2278
|
-
return api.post(`/tunnels/${encodeURIComponent(id)}/test`);
|
|
2279
|
-
},
|
|
2280
|
-
|
|
2281
|
-
async connectTunnel(id) {
|
|
2282
|
-
try {
|
|
2283
|
-
const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
|
|
2284
|
-
get().fetchTunnels();
|
|
2285
|
-
if (result.localPort && result.name) {
|
|
2286
|
-
if (window.groove?.remote?.openWindow) {
|
|
2287
|
-
window.groove.remote.openWindow(result.localPort, result.name);
|
|
2288
|
-
} else {
|
|
2289
|
-
window.open(`http://localhost:${result.localPort}?instance=${encodeURIComponent(result.name)}`, '_blank');
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
return result;
|
|
2293
|
-
} finally {
|
|
2294
|
-
set({ tunnelConnectStep: null });
|
|
2295
|
-
}
|
|
2296
|
-
},
|
|
2297
|
-
|
|
2298
|
-
async upgradeTunnel(id) {
|
|
2299
|
-
return api.post(`/tunnels/${encodeURIComponent(id)}/upgrade`);
|
|
2300
|
-
},
|
|
2301
|
-
|
|
2302
|
-
async disconnectTunnel(id) {
|
|
2303
|
-
const tunnel = get().savedTunnels.find(t => t.id === id);
|
|
2304
|
-
await api.post(`/tunnels/${encodeURIComponent(id)}/disconnect`);
|
|
2305
|
-
get().fetchTunnels();
|
|
2306
|
-
if (tunnel?.localPort && window.groove?.remote?.closeByPort) {
|
|
2307
|
-
window.groove.remote.closeByPort(tunnel.localPort);
|
|
2308
|
-
}
|
|
2309
|
-
},
|
|
2310
|
-
|
|
2311
|
-
async installTunnel(id) {
|
|
2312
|
-
return api.post(`/tunnels/${encodeURIComponent(id)}/install`);
|
|
2313
|
-
},
|
|
2314
|
-
|
|
2315
|
-
async startTunnel(id) {
|
|
2316
|
-
return api.post(`/tunnels/${encodeURIComponent(id)}/start`);
|
|
2317
|
-
},
|
|
2318
|
-
|
|
2319
|
-
// ── Journalist ────────────────────────────────────────────
|
|
2320
|
-
|
|
2321
|
-
async fetchJournalist() {
|
|
2322
|
-
try {
|
|
2323
|
-
const data = await api.get('/journalist');
|
|
2324
|
-
set({ journalistStatus: data });
|
|
2325
|
-
return data;
|
|
2326
|
-
} catch { return null; }
|
|
2327
|
-
},
|
|
2328
|
-
|
|
2329
|
-
async triggerJournalistCycle() {
|
|
2330
|
-
try {
|
|
2331
|
-
const data = await api.post('/journalist/cycle');
|
|
2332
|
-
get().addToast('success', 'Synthesis cycle triggered');
|
|
2333
|
-
set({ journalistStatus: data });
|
|
2334
|
-
return data;
|
|
2335
|
-
} catch (err) {
|
|
2336
|
-
get().addToast('error', 'Synthesis failed', err.message);
|
|
2337
|
-
throw err;
|
|
2338
|
-
}
|
|
2339
|
-
},
|
|
2340
|
-
|
|
2341
|
-
// ── Agent Actions ─────────────────────────────────────────
|
|
2342
|
-
|
|
2343
|
-
async spawnAgent(config) {
|
|
2344
|
-
try {
|
|
2345
|
-
const teamId = get().activeTeamId;
|
|
2346
|
-
const agent = await api.post('/agents', { ...config, teamId });
|
|
2347
|
-
get().addToast('success', `Spawned ${agent.name}`);
|
|
2348
|
-
return agent;
|
|
2349
|
-
} catch (err) {
|
|
2350
|
-
let detail = err.message;
|
|
2351
|
-
if (detail?.includes('workingDir must be within project directory')) {
|
|
2352
|
-
const projDir = get().projectDir || 'unknown';
|
|
2353
|
-
const workDir = config.workingDir || 'default';
|
|
2354
|
-
detail = `workingDir "${workDir}" is outside project directory "${projDir}". Change the project directory or pick a subfolder within it.`;
|
|
2355
|
-
}
|
|
2356
|
-
get().addToast('error', 'Spawn failed', detail);
|
|
2357
|
-
throw err;
|
|
2358
|
-
}
|
|
2359
|
-
},
|
|
2360
|
-
|
|
2361
|
-
async killAgent(id, purge = false) {
|
|
2362
|
-
try {
|
|
2363
|
-
await api.delete(`/agents/${encodeURIComponent(id)}?purge=${purge}`);
|
|
2364
|
-
if (purge) {
|
|
2365
|
-
set((s) => {
|
|
2366
|
-
const chatHistory = { ...s.chatHistory };
|
|
2367
|
-
const activityLog = { ...s.activityLog };
|
|
2368
|
-
const tokenTimeline = { ...s.tokenTimeline };
|
|
2369
|
-
delete chatHistory[id];
|
|
2370
|
-
delete activityLog[id];
|
|
2371
|
-
delete tokenTimeline[id];
|
|
2372
|
-
persistJSON('groove:chatHistory', chatHistory);
|
|
2373
|
-
persistJSON('groove:activityLog', activityLog);
|
|
2374
|
-
return { chatHistory, activityLog, tokenTimeline };
|
|
2375
|
-
});
|
|
2376
|
-
}
|
|
2377
|
-
} catch (err) {
|
|
2378
|
-
get().addToast('error', 'Kill failed', err.message);
|
|
2379
|
-
}
|
|
2380
|
-
},
|
|
2381
|
-
|
|
2382
|
-
async rotateAgent(id) {
|
|
2383
|
-
try {
|
|
2384
|
-
return await api.post(`/agents/${encodeURIComponent(id)}/rotate`);
|
|
2385
|
-
} catch (err) {
|
|
2386
|
-
get().addToast('error', 'Rotation failed', err.message);
|
|
2387
|
-
throw err;
|
|
2388
|
-
}
|
|
2389
|
-
},
|
|
2390
|
-
|
|
2391
|
-
async fetchProviders() {
|
|
2392
|
-
return api.get('/providers');
|
|
2393
|
-
},
|
|
2394
|
-
|
|
2395
|
-
// ── Local Models (Ollama) ─────────────────────────────────
|
|
2396
|
-
|
|
2397
|
-
async fetchOllamaStatus() {
|
|
2398
|
-
try {
|
|
2399
|
-
const check = await api.post('/providers/ollama/check');
|
|
2400
|
-
const updates = {
|
|
2401
|
-
ollamaStatus: { installed: check.installed, serverRunning: check.serverRunning, hardware: check.hardware },
|
|
2402
|
-
};
|
|
2403
|
-
if (check.installed) {
|
|
2404
|
-
try {
|
|
2405
|
-
const models = await api.get('/providers/ollama/models');
|
|
2406
|
-
updates.ollamaInstalledModels = models.installed || [];
|
|
2407
|
-
updates.ollamaCatalog = models.catalog || [];
|
|
2408
|
-
} catch {}
|
|
2409
|
-
}
|
|
2410
|
-
if (check.serverRunning) {
|
|
2411
|
-
try {
|
|
2412
|
-
const running = await api.get('/providers/ollama/running');
|
|
2413
|
-
updates.ollamaRunningModels = running.models || [];
|
|
2414
|
-
} catch {
|
|
2415
|
-
updates.ollamaRunningModels = [];
|
|
2416
|
-
}
|
|
2417
|
-
} else {
|
|
2418
|
-
updates.ollamaRunningModels = [];
|
|
2419
|
-
}
|
|
2420
|
-
set(updates);
|
|
2421
|
-
return updates.ollamaStatus;
|
|
2422
|
-
} catch {
|
|
2423
|
-
return get().ollamaStatus;
|
|
2424
|
-
}
|
|
2425
|
-
},
|
|
2426
|
-
|
|
2427
|
-
async startOllamaServer() {
|
|
2428
|
-
try {
|
|
2429
|
-
const result = await api.post('/providers/ollama/serve');
|
|
2430
|
-
if (result.ok) {
|
|
2431
|
-
get().addToast('success', 'Ollama server started');
|
|
2432
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
2433
|
-
await get().fetchOllamaStatus();
|
|
2434
|
-
}
|
|
2435
|
-
return result;
|
|
2436
|
-
} catch (err) {
|
|
2437
|
-
get().addToast('error', 'Could not start server', err.message);
|
|
2438
|
-
throw err;
|
|
2439
|
-
}
|
|
2440
|
-
},
|
|
2441
|
-
|
|
2442
|
-
async stopOllamaServer() {
|
|
2443
|
-
try {
|
|
2444
|
-
const result = await api.post('/providers/ollama/stop');
|
|
2445
|
-
if (result.ok) {
|
|
2446
|
-
get().addToast('info', 'Ollama server stopped');
|
|
2447
|
-
set((s) => ({
|
|
2448
|
-
ollamaStatus: { ...s.ollamaStatus, serverRunning: false },
|
|
2449
|
-
ollamaRunningModels: [],
|
|
2450
|
-
}));
|
|
2451
|
-
}
|
|
2452
|
-
return result;
|
|
2453
|
-
} catch (err) {
|
|
2454
|
-
get().addToast('error', 'Stop failed', err.message);
|
|
2455
|
-
throw err;
|
|
2456
|
-
}
|
|
2457
|
-
},
|
|
2458
|
-
|
|
2459
|
-
async restartOllamaServer() {
|
|
2460
|
-
try {
|
|
2461
|
-
const result = await api.post('/providers/ollama/restart');
|
|
2462
|
-
if (result.ok) {
|
|
2463
|
-
get().addToast('success', 'Ollama server restarted');
|
|
2464
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
2465
|
-
await get().fetchOllamaStatus();
|
|
2466
|
-
}
|
|
2467
|
-
return result;
|
|
2468
|
-
} catch (err) {
|
|
2469
|
-
get().addToast('error', 'Restart failed', err.message);
|
|
2470
|
-
throw err;
|
|
2471
|
-
}
|
|
2472
|
-
},
|
|
2473
|
-
|
|
2474
|
-
async pullOllamaModel(modelId) {
|
|
2475
|
-
try {
|
|
2476
|
-
set((s) => ({ ollamaPullProgress: { ...s.ollamaPullProgress, [modelId]: { status: 'pulling', progress: '' } } }));
|
|
2477
|
-
await api.post('/providers/ollama/pull', { model: modelId });
|
|
2478
|
-
set((s) => {
|
|
2479
|
-
const progress = { ...s.ollamaPullProgress };
|
|
2480
|
-
delete progress[modelId];
|
|
2481
|
-
return { ollamaPullProgress: progress };
|
|
2482
|
-
});
|
|
2483
|
-
get().addToast('success', `${modelId} ready to use`);
|
|
2484
|
-
get().fetchOllamaStatus();
|
|
2485
|
-
} catch (err) {
|
|
2486
|
-
set((s) => {
|
|
2487
|
-
const progress = { ...s.ollamaPullProgress };
|
|
2488
|
-
delete progress[modelId];
|
|
2489
|
-
return { ollamaPullProgress: progress };
|
|
2490
|
-
});
|
|
2491
|
-
get().addToast('error', `Pull failed: ${err.message}`);
|
|
2492
|
-
}
|
|
2493
|
-
},
|
|
2494
|
-
|
|
2495
|
-
async deleteOllamaModel(modelId) {
|
|
2496
|
-
try {
|
|
2497
|
-
await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
|
|
2498
|
-
set((s) => ({ ollamaInstalledModels: s.ollamaInstalledModels.filter((m) => m.id !== modelId) }));
|
|
2499
|
-
get().addToast('success', `Removed ${modelId}`);
|
|
2500
|
-
} catch (err) {
|
|
2501
|
-
get().addToast('error', `Delete failed: ${err.message}`);
|
|
2502
|
-
}
|
|
2503
|
-
},
|
|
2504
|
-
|
|
2505
|
-
async loadOllamaModel(modelId) {
|
|
2506
|
-
try {
|
|
2507
|
-
await api.post('/providers/ollama/load', { model: modelId });
|
|
2508
|
-
get().addToast('success', `${modelId} loaded into memory`);
|
|
2509
|
-
get().fetchOllamaStatus();
|
|
2510
|
-
} catch (err) {
|
|
2511
|
-
get().addToast('error', `Could not load model: ${err.message}`);
|
|
2512
|
-
}
|
|
2513
|
-
},
|
|
2514
|
-
|
|
2515
|
-
async unloadOllamaModel(modelId) {
|
|
2516
|
-
try {
|
|
2517
|
-
await api.post('/providers/ollama/unload', { model: modelId });
|
|
2518
|
-
set((s) => ({ ollamaRunningModels: s.ollamaRunningModels.filter((m) => m.name !== modelId) }));
|
|
2519
|
-
get().addToast('info', `${modelId} unloaded`);
|
|
2520
|
-
} catch (err) {
|
|
2521
|
-
get().addToast('error', `Unload failed: ${err.message}`);
|
|
2522
|
-
}
|
|
2523
|
-
},
|
|
2524
|
-
|
|
2525
|
-
spawnFromModel(modelId) {
|
|
2526
|
-
get().openDetail({ type: 'spawn', presetProvider: 'ollama', presetModel: modelId });
|
|
2527
|
-
},
|
|
2528
|
-
|
|
2529
|
-
// ── Onboarding ────────────────────────────────────────────
|
|
2530
|
-
|
|
2531
|
-
async fetchOnboardingStatus() {
|
|
2532
|
-
try {
|
|
2533
|
-
const data = await api.get('/onboarding/status');
|
|
2534
|
-
if (data?.complete) {
|
|
2535
|
-
set({ onboardingComplete: true });
|
|
2536
|
-
localStorage.setItem('groove:onboardingComplete', 'true');
|
|
2537
|
-
}
|
|
2538
|
-
return data;
|
|
2539
|
-
} catch {
|
|
2540
|
-
return null;
|
|
2541
|
-
}
|
|
2542
|
-
},
|
|
2543
|
-
|
|
2544
|
-
dismissOnboarding() {
|
|
2545
|
-
set({ onboardingComplete: true });
|
|
2546
|
-
localStorage.setItem('groove:onboardingComplete', 'true');
|
|
2547
|
-
api.post('/onboarding/dismiss').catch(() => {});
|
|
2548
|
-
},
|
|
2549
|
-
|
|
2550
|
-
// ── Provider Setup (Settings) ──────────────────────────────
|
|
2551
|
-
|
|
2552
|
-
providerInstallProgress: {},
|
|
2553
|
-
|
|
2554
|
-
async installProvider(providerId) {
|
|
2555
|
-
const update = (patch) => set((s) => ({
|
|
2556
|
-
providerInstallProgress: {
|
|
2557
|
-
...s.providerInstallProgress,
|
|
2558
|
-
[providerId]: { ...s.providerInstallProgress[providerId], ...patch },
|
|
2559
|
-
},
|
|
2560
|
-
}));
|
|
2561
|
-
|
|
2562
|
-
update({ installing: true, percent: 0, message: 'Starting install...', error: null, done: false });
|
|
2563
|
-
|
|
2564
|
-
try {
|
|
2565
|
-
const res = await fetch(`/api/providers/${encodeURIComponent(providerId)}/install`, {
|
|
2566
|
-
method: 'POST',
|
|
2567
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2568
|
-
});
|
|
2569
|
-
if (!res.ok) {
|
|
2570
|
-
const err = await res.text();
|
|
2571
|
-
throw new Error(err || `Install failed (${res.status})`);
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
let body;
|
|
2575
|
-
try {
|
|
2576
|
-
body = await res.text();
|
|
2577
|
-
} catch (e) {
|
|
2578
|
-
throw new Error(`Failed to read response: ${e.message}`);
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
let lastError = null;
|
|
2582
|
-
let completed = false;
|
|
2583
|
-
for (const line of body.split('\n')) {
|
|
2584
|
-
if (!line.trim()) continue;
|
|
2585
|
-
try {
|
|
2586
|
-
const ev = JSON.parse(line);
|
|
2587
|
-
const isError = ev.status === 'error';
|
|
2588
|
-
const isDone = ev.status === 'complete';
|
|
2589
|
-
if (isError) lastError = ev.output || 'Install failed';
|
|
2590
|
-
if (isDone) completed = true;
|
|
2591
|
-
update({
|
|
2592
|
-
percent: ev.progress ?? get().providerInstallProgress[providerId]?.percent ?? 0,
|
|
2593
|
-
message: ev.output || get().providerInstallProgress[providerId]?.message,
|
|
2594
|
-
error: isError ? (ev.output || 'Install failed') : null,
|
|
2595
|
-
done: isDone,
|
|
2596
|
-
installing: !isDone && !isError,
|
|
2597
|
-
});
|
|
2598
|
-
} catch { /* skip malformed line */ }
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
if (lastError) throw new Error(lastError);
|
|
2602
|
-
if (!completed) throw new Error(body.slice(0, 500) || 'Install ended without confirmation');
|
|
2603
|
-
|
|
2604
|
-
update({ installing: false, percent: 100, message: 'Installed', error: null, done: true });
|
|
2605
|
-
set({ _providerRefreshTick: Date.now() });
|
|
2606
|
-
get().addToast('success', `${providerId} installed`);
|
|
2607
|
-
} catch (err) {
|
|
2608
|
-
update({ installing: false, percent: 0, message: null, error: err.message, done: false });
|
|
2609
|
-
get().addToast('error', `Install failed: ${providerId}`, err.message);
|
|
2610
|
-
throw err;
|
|
2611
|
-
}
|
|
2612
|
-
},
|
|
2613
|
-
|
|
2614
|
-
async loginProvider(providerId, body) {
|
|
2615
|
-
try {
|
|
2616
|
-
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/login`, body);
|
|
2617
|
-
if (data?.url && !data?.browserOpened) window.open(data.url, '_blank');
|
|
2618
|
-
return data;
|
|
2619
|
-
} catch (err) {
|
|
2620
|
-
get().addToast('error', `Login failed`, err.message);
|
|
2621
|
-
throw err;
|
|
2622
|
-
}
|
|
2623
|
-
},
|
|
2624
|
-
|
|
2625
|
-
async setProviderPath(providerId, path) {
|
|
2626
|
-
try {
|
|
2627
|
-
await api.post(`/providers/${encodeURIComponent(providerId)}/set-path`, { path });
|
|
2628
|
-
get().addToast('success', `Custom path set for ${providerId}`);
|
|
2629
|
-
} catch (err) {
|
|
2630
|
-
get().addToast('error', 'Failed to set path', err.message);
|
|
2631
|
-
throw err;
|
|
2632
|
-
}
|
|
2633
|
-
},
|
|
2634
|
-
|
|
2635
|
-
async verifyProvider(providerId) {
|
|
2636
|
-
try {
|
|
2637
|
-
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/verify`);
|
|
2638
|
-
return data;
|
|
2639
|
-
} catch (err) {
|
|
2640
|
-
get().addToast('error', `Verification failed`, err.message);
|
|
2641
|
-
throw err;
|
|
2642
|
-
}
|
|
2643
|
-
},
|
|
2644
|
-
|
|
2645
|
-
async setDefaultProvider(provider, model) {
|
|
2646
|
-
try {
|
|
2647
|
-
await api.post('/onboarding/set-default', { provider, model });
|
|
2648
|
-
get().addToast('success', `Default set to ${provider} (${model})`);
|
|
2649
|
-
} catch (err) {
|
|
2650
|
-
get().addToast('error', 'Failed to set default', err.message);
|
|
2651
|
-
throw err;
|
|
2652
|
-
}
|
|
2653
|
-
},
|
|
2654
|
-
|
|
2655
|
-
// ── Chat ──────────────────────────────────────────────────
|
|
2656
|
-
|
|
2657
|
-
addChatMessage(agentId, from, text, isQuery = false) {
|
|
2658
|
-
set((s) => {
|
|
2659
|
-
const history = { ...s.chatHistory };
|
|
2660
|
-
if (!history[agentId]) history[agentId] = [];
|
|
2661
|
-
history[agentId] = [...history[agentId].slice(-100), { from, text, timestamp: Date.now(), isQuery }];
|
|
2662
|
-
persistJSON('groove:chatHistory', history);
|
|
2663
|
-
return { chatHistory: history };
|
|
2664
|
-
});
|
|
2665
|
-
},
|
|
2666
|
-
|
|
2667
|
-
// Track which agents are thinking (sent a message, waiting for response)
|
|
2668
|
-
thinkingAgents: new Set(),
|
|
2669
|
-
|
|
2670
|
-
async stopAgent(id) {
|
|
2671
|
-
try {
|
|
2672
|
-
await api.post(`/agents/${encodeURIComponent(id)}/stop`);
|
|
2673
|
-
// Clear thinking indicator
|
|
2674
|
-
set((s) => {
|
|
2675
|
-
const next = new Set(s.thinkingAgents);
|
|
2676
|
-
next.delete(id);
|
|
2677
|
-
return { thinkingAgents: next };
|
|
2678
|
-
});
|
|
2679
|
-
get().addToast('info', 'Stopped agent');
|
|
2680
|
-
} catch (err) {
|
|
2681
|
-
get().addToast('error', 'Stop failed', err.message);
|
|
2682
|
-
}
|
|
2683
|
-
},
|
|
2684
|
-
|
|
2685
|
-
async instructAgent(id, message) {
|
|
2686
|
-
// ── Keeper command interception ─────────────────────────
|
|
2687
|
-
const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
|
|
2688
|
-
if (keeperCmd) {
|
|
2689
|
-
const handled = await get()._handleKeeperCommand(id, message, keeperCmd[1].toLowerCase());
|
|
2690
|
-
if (handled === true) return { status: 'keeper_handled' };
|
|
2691
|
-
if (handled?.passthrough) {
|
|
2692
|
-
message = handled.passthrough;
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
get().addChatMessage(id, 'user', message, false);
|
|
2697
|
-
set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
|
|
2698
|
-
|
|
2699
|
-
// Auto-attach active file context when in workspace mode
|
|
2700
|
-
let enriched = message;
|
|
2701
|
-
if (get().workspaceMode && get().workspaceAgentId === id && get().editorActiveFile) {
|
|
2702
|
-
const filePath = get().editorActiveFile;
|
|
2703
|
-
enriched = `[Active file: ${filePath}]\n\n${message}`;
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
const snapshot = {
|
|
2707
|
-
chatHistory: [...(get().chatHistory[id] || [])],
|
|
2708
|
-
activityLog: [...(get().activityLog[id] || [])],
|
|
2709
|
-
tokenTimeline: [...(get().tokenTimeline[id] || [])],
|
|
2710
|
-
};
|
|
2711
|
-
|
|
2712
|
-
try {
|
|
2713
|
-
const data = await api.post(`/agents/${encodeURIComponent(id)}/instruct`, { message: enriched });
|
|
2714
|
-
|
|
2715
|
-
if (data.status === 'message_sent') {
|
|
2716
|
-
return data;
|
|
2717
|
-
}
|
|
2718
|
-
if (data.status === 'message_queued') {
|
|
2719
|
-
set((s) => {
|
|
2720
|
-
const next = new Set(s.thinkingAgents);
|
|
2721
|
-
next.delete(id);
|
|
2722
|
-
return { thinkingAgents: next };
|
|
2723
|
-
});
|
|
2724
|
-
return data;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
// CLI agent: was stopped + resumed/rotated — transfer state to new agent ID
|
|
2728
|
-
const newAgent = data;
|
|
2729
|
-
for (const key of ['chatHistory', 'activityLog', 'tokenTimeline']) {
|
|
2730
|
-
if (snapshot[key]?.length) {
|
|
2731
|
-
set((s) => ({ [key]: { ...s[key], [newAgent.id]: [...snapshot[key]] } }));
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
2734
|
-
set((s) => {
|
|
2735
|
-
const next = new Set(s.thinkingAgents);
|
|
2736
|
-
next.delete(id);
|
|
2737
|
-
next.add(newAgent.id);
|
|
2738
|
-
return { thinkingAgents: next };
|
|
2739
|
-
});
|
|
2740
|
-
if (get().chatHistory[newAgent.id]?.length) persistJSON('groove:chatHistory', get().chatHistory);
|
|
2741
|
-
if (get().activityLog[newAgent.id]?.length) persistJSON('groove:activityLog', get().activityLog);
|
|
2742
|
-
get().selectAgent(newAgent.id);
|
|
2743
|
-
return newAgent;
|
|
2744
|
-
} catch (err) {
|
|
2745
|
-
set((s) => {
|
|
2746
|
-
const next = new Set(s.thinkingAgents);
|
|
2747
|
-
next.delete(id);
|
|
2748
|
-
return { thinkingAgents: next };
|
|
2749
|
-
});
|
|
2750
|
-
get().addChatMessage(id, 'system', `failed: ${err.message}`);
|
|
2751
|
-
throw err;
|
|
2752
|
-
}
|
|
2753
|
-
},
|
|
2754
|
-
|
|
2755
|
-
async queryAgent(id, message) {
|
|
2756
|
-
get().addChatMessage(id, 'user', message, true);
|
|
2757
|
-
try {
|
|
2758
|
-
const data = await api.post(`/agents/${encodeURIComponent(id)}/query`, { message });
|
|
2759
|
-
get().addChatMessage(id, 'agent', data.response);
|
|
2760
|
-
return data;
|
|
2761
|
-
} catch (err) {
|
|
2762
|
-
get().addChatMessage(id, 'system', `query failed: ${err.message}`);
|
|
2763
|
-
throw err;
|
|
2764
|
-
}
|
|
2765
|
-
},
|
|
2766
|
-
|
|
2767
|
-
// ── Conversations (Chat view) ────────────────────────────
|
|
2768
|
-
|
|
2769
|
-
async fetchConversations() {
|
|
2770
|
-
try {
|
|
2771
|
-
const data = await api.get('/conversations');
|
|
2772
|
-
set({ conversations: data.conversations || data || [] });
|
|
2773
|
-
} catch { /* endpoint may not exist yet */ }
|
|
2774
|
-
},
|
|
2775
|
-
|
|
2776
|
-
async createConversation(provider, model, mode = 'api') {
|
|
2777
|
-
try {
|
|
2778
|
-
const conv = await api.post('/conversations', { provider, model, mode });
|
|
2779
|
-
set((s) => ({
|
|
2780
|
-
conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)],
|
|
2781
|
-
activeConversationId: conv.id,
|
|
2782
|
-
}));
|
|
2783
|
-
localStorage.setItem('groove:activeConversationId', conv.id);
|
|
2784
|
-
return conv;
|
|
2785
|
-
} catch (err) {
|
|
2786
|
-
get().addToast('error', 'Failed to create conversation', err.message);
|
|
2787
|
-
throw err;
|
|
2788
|
-
}
|
|
2789
|
-
},
|
|
2790
|
-
|
|
2791
|
-
async setConversationMode(id, mode) {
|
|
2792
|
-
if (_modeChangePending.has(id)) return;
|
|
2793
|
-
_modeChangePending.add(id);
|
|
2794
|
-
try {
|
|
2795
|
-
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { mode });
|
|
2796
|
-
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
2797
|
-
} catch (err) {
|
|
2798
|
-
get().addToast('error', 'Mode change failed', err.message);
|
|
2799
|
-
} finally {
|
|
2800
|
-
_modeChangePending.delete(id);
|
|
2801
|
-
}
|
|
2802
|
-
},
|
|
2803
|
-
|
|
2804
|
-
async setConversationModel(id, provider, model) {
|
|
2805
|
-
try {
|
|
2806
|
-
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { provider, model });
|
|
2807
|
-
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
2808
|
-
} catch (err) {
|
|
2809
|
-
get().addToast('error', 'Model change failed', err.message);
|
|
2810
|
-
}
|
|
2811
|
-
},
|
|
2812
|
-
|
|
2813
|
-
async stopChatStreaming(conversationId) {
|
|
2814
|
-
try {
|
|
2815
|
-
await api.post(`/conversations/${encodeURIComponent(conversationId)}/stop`);
|
|
2816
|
-
set({ sendingMessage: false, streamingConversationId: null });
|
|
2817
|
-
} catch { /* ignore */ }
|
|
2818
|
-
},
|
|
2819
|
-
|
|
2820
|
-
async deleteConversation(id) {
|
|
2821
|
-
try {
|
|
2822
|
-
await api.delete(`/conversations/${encodeURIComponent(id)}`);
|
|
2823
|
-
set((s) => {
|
|
2824
|
-
const conversations = s.conversations.filter((c) => c.id !== id);
|
|
2825
|
-
const conversationMessages = { ...s.conversationMessages };
|
|
2826
|
-
delete conversationMessages[id];
|
|
2827
|
-
persistJSON('groove:conversationMessages', conversationMessages);
|
|
2828
|
-
const activeConversationId = s.activeConversationId === id
|
|
2829
|
-
? (conversations[0]?.id || null)
|
|
2830
|
-
: s.activeConversationId;
|
|
2831
|
-
localStorage.setItem('groove:activeConversationId', activeConversationId || '');
|
|
2832
|
-
const conversationRoles = { ...s.conversationRoles };
|
|
2833
|
-
delete conversationRoles[id];
|
|
2834
|
-
persistJSON('groove:conversationRoles', conversationRoles);
|
|
2835
|
-
const conversationReasoningEffort = { ...s.conversationReasoningEffort };
|
|
2836
|
-
delete conversationReasoningEffort[id];
|
|
2837
|
-
persistJSON('groove:conversationReasoningEffort', conversationReasoningEffort);
|
|
2838
|
-
const conversationVerbosity = { ...s.conversationVerbosity };
|
|
2839
|
-
delete conversationVerbosity[id];
|
|
2840
|
-
persistJSON('groove:conversationVerbosity', conversationVerbosity);
|
|
2841
|
-
return { conversations, conversationMessages, conversationRoles, conversationReasoningEffort, conversationVerbosity, activeConversationId };
|
|
2842
|
-
});
|
|
2843
|
-
} catch (err) {
|
|
2844
|
-
get().addToast('error', 'Delete failed', err.message);
|
|
2845
|
-
}
|
|
2846
|
-
},
|
|
2847
|
-
|
|
2848
|
-
async renameConversation(id, title) {
|
|
2849
|
-
try {
|
|
2850
|
-
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { title });
|
|
2851
|
-
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
2852
|
-
} catch (err) {
|
|
2853
|
-
get().addToast('error', 'Rename failed', err.message);
|
|
2854
|
-
}
|
|
2855
|
-
},
|
|
2856
|
-
|
|
2857
|
-
async pinConversation(id, pinned) {
|
|
2858
|
-
try {
|
|
2859
|
-
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { pinned });
|
|
2860
|
-
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
2861
|
-
} catch (err) {
|
|
2862
|
-
get().addToast('error', 'Pin failed', err.message);
|
|
2863
|
-
}
|
|
2864
|
-
},
|
|
2865
|
-
|
|
2866
|
-
setActiveConversation(id) {
|
|
2867
|
-
set({ activeConversationId: id });
|
|
2868
|
-
localStorage.setItem('groove:activeConversationId', id || '');
|
|
2869
|
-
},
|
|
2870
|
-
|
|
2871
|
-
setConversationRole(id, role) {
|
|
2872
|
-
set((s) => {
|
|
2873
|
-
const roles = { ...s.conversationRoles };
|
|
2874
|
-
if (role) {
|
|
2875
|
-
roles[id] = role;
|
|
2876
|
-
} else {
|
|
2877
|
-
delete roles[id];
|
|
2878
|
-
}
|
|
2879
|
-
persistJSON('groove:conversationRoles', roles);
|
|
2880
|
-
return { conversationRoles: roles };
|
|
2881
|
-
});
|
|
2882
|
-
},
|
|
2883
|
-
|
|
2884
|
-
setConversationReasoningEffort(id, effort) {
|
|
2885
|
-
set((s) => {
|
|
2886
|
-
const map = { ...s.conversationReasoningEffort };
|
|
2887
|
-
map[id] = effort || 'medium';
|
|
2888
|
-
persistJSON('groove:conversationReasoningEffort', map);
|
|
2889
|
-
return { conversationReasoningEffort: map };
|
|
2890
|
-
});
|
|
2891
|
-
},
|
|
2892
|
-
|
|
2893
|
-
setConversationVerbosity(id, verbosity) {
|
|
2894
|
-
set((s) => {
|
|
2895
|
-
const map = { ...s.conversationVerbosity };
|
|
2896
|
-
map[id] = verbosity || 'medium';
|
|
2897
|
-
persistJSON('groove:conversationVerbosity', map);
|
|
2898
|
-
return { conversationVerbosity: map };
|
|
2899
|
-
});
|
|
2900
|
-
},
|
|
2901
|
-
|
|
2902
|
-
async sendChatMessage(conversationId, message) {
|
|
2903
|
-
const conv = get().conversations.find((c) => c.id === conversationId);
|
|
2904
|
-
if (!conv) return;
|
|
2905
|
-
|
|
2906
|
-
// Add user message to local state immediately
|
|
2907
|
-
set((s) => {
|
|
2908
|
-
const msgs = { ...s.conversationMessages };
|
|
2909
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2910
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: message, timestamp: Date.now() }];
|
|
2911
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
2912
|
-
return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
|
|
2913
|
-
});
|
|
2914
|
-
|
|
2915
|
-
try {
|
|
2916
|
-
const body = { message };
|
|
2917
|
-
if (conv.mode === 'api' || !conv.mode) {
|
|
2918
|
-
const history = get().conversationMessages[conversationId] || [];
|
|
2919
|
-
body.history = history.slice(0, -1);
|
|
2920
|
-
|
|
2921
|
-
const role = get().conversationRoles?.[conversationId];
|
|
2922
|
-
const rules = ['Never use emojis in your responses.', 'Be professional, concise, and direct.'];
|
|
2923
|
-
if (role) rules.unshift(`You are a professional ${role}. Respond with deep expertise in that domain.`);
|
|
2924
|
-
const systemCtx = rules.join(' ');
|
|
2925
|
-
body.history = [
|
|
2926
|
-
{ from: 'user', text: `Instructions: ${systemCtx}` },
|
|
2927
|
-
{ from: 'assistant', text: 'Understood.' },
|
|
2928
|
-
...body.history,
|
|
2929
|
-
];
|
|
2930
|
-
}
|
|
2931
|
-
const effort = get().conversationReasoningEffort?.[conversationId] || 'medium';
|
|
2932
|
-
const verbosity = get().conversationVerbosity?.[conversationId] || 'medium';
|
|
2933
|
-
if (conv.provider === 'codex') {
|
|
2934
|
-
body.reasoning_effort = effort;
|
|
2935
|
-
body.verbosity = verbosity;
|
|
2936
|
-
}
|
|
2937
|
-
await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
|
|
2938
|
-
} catch (err) {
|
|
2939
|
-
set((s) => {
|
|
2940
|
-
const msgs = { ...s.conversationMessages };
|
|
2941
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2942
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Failed: ${err.message}`, timestamp: Date.now() }];
|
|
2943
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
2944
|
-
return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
|
|
2945
|
-
});
|
|
2946
|
-
get().addToast('error', 'Message failed', err.message);
|
|
2947
|
-
}
|
|
2948
|
-
},
|
|
2949
|
-
|
|
2950
|
-
async sendImageMessage(conversationId, prompt, { model, size, quality } = {}) {
|
|
2951
|
-
const conv = get().conversations.find((c) => c.id === conversationId);
|
|
2952
|
-
if (!conv) return;
|
|
2953
|
-
|
|
2954
|
-
set((s) => {
|
|
2955
|
-
const msgs = { ...s.conversationMessages };
|
|
2956
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2957
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: prompt, timestamp: Date.now() }];
|
|
2958
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
2959
|
-
return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
|
|
2960
|
-
});
|
|
2961
|
-
|
|
2962
|
-
try {
|
|
2963
|
-
await api.post(`/conversations/${encodeURIComponent(conversationId)}/generate-image`, { prompt, model, size, quality });
|
|
2964
|
-
} catch (err) {
|
|
2965
|
-
set((s) => {
|
|
2966
|
-
const msgs = { ...s.conversationMessages };
|
|
2967
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2968
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Image failed: ${err.message}`, timestamp: Date.now() }];
|
|
2969
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
2970
|
-
return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
|
|
2971
|
-
});
|
|
2972
|
-
get().addToast('error', 'Image generation failed', err.message);
|
|
2973
|
-
}
|
|
2974
|
-
},
|
|
2975
|
-
|
|
2976
|
-
// ── Editor ────────────────────────────────────────────────
|
|
2977
|
-
|
|
2978
|
-
async openFile(path) {
|
|
2979
|
-
if (get().editorFiles[path] || get().editorOpenTabs.includes(path)) {
|
|
2980
|
-
set((s) => ({
|
|
2981
|
-
editorActiveFile: path,
|
|
2982
|
-
editorOpenTabs: s.editorOpenTabs.includes(path) ? s.editorOpenTabs : [...s.editorOpenTabs, path],
|
|
2983
|
-
}));
|
|
2984
|
-
return;
|
|
2985
|
-
}
|
|
2986
|
-
const ext = path.split('.').pop()?.toLowerCase();
|
|
2987
|
-
const MEDIA = ['png','jpg','jpeg','gif','svg','webp','ico','bmp','avif','mp4','webm','mov','avi','mkv','ogv'];
|
|
2988
|
-
if (MEDIA.includes(ext)) {
|
|
2989
|
-
set((s) => ({ editorActiveFile: path, editorOpenTabs: [...s.editorOpenTabs, path] }));
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
|
-
try {
|
|
2993
|
-
const data = await api.get(`/files/read?path=${encodeURIComponent(path)}`);
|
|
2994
|
-
if (data.binary) { get().addToast('warning', 'Binary file — cannot open'); return; }
|
|
2995
|
-
set((s) => ({
|
|
2996
|
-
editorFiles: { ...s.editorFiles, [path]: { content: data.content, originalContent: data.content, language: data.language, loadedAt: Date.now() } },
|
|
2997
|
-
editorActiveFile: path,
|
|
2998
|
-
editorOpenTabs: s.editorOpenTabs.includes(path) ? s.editorOpenTabs : [...s.editorOpenTabs, path],
|
|
2999
|
-
}));
|
|
3000
|
-
const ws = get().ws;
|
|
3001
|
-
if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watch', path }));
|
|
3002
|
-
} catch (err) {
|
|
3003
|
-
get().addToast('error', 'Failed to open file', err.message);
|
|
3004
|
-
}
|
|
3005
|
-
},
|
|
3006
|
-
|
|
3007
|
-
closeFile(path) {
|
|
3008
|
-
set((s) => {
|
|
3009
|
-
const tabs = s.editorOpenTabs.filter((t) => t !== path);
|
|
3010
|
-
const files = { ...s.editorFiles };
|
|
3011
|
-
delete files[path];
|
|
3012
|
-
const changed = { ...s.editorChangedFiles };
|
|
3013
|
-
delete changed[path];
|
|
3014
|
-
let active = s.editorActiveFile;
|
|
3015
|
-
if (active === path) {
|
|
3016
|
-
const idx = s.editorOpenTabs.indexOf(path);
|
|
3017
|
-
active = tabs[Math.min(idx, tabs.length - 1)] || null;
|
|
3018
|
-
}
|
|
3019
|
-
return { editorOpenTabs: tabs, editorFiles: files, editorChangedFiles: changed, editorActiveFile: active };
|
|
3020
|
-
});
|
|
3021
|
-
const ws = get().ws;
|
|
3022
|
-
if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:unwatch', path }));
|
|
3023
|
-
},
|
|
3024
|
-
|
|
3025
|
-
setActiveFile(path) { set({ editorActiveFile: path }); },
|
|
3026
|
-
|
|
3027
|
-
setEditorSidebarWidth(width) {
|
|
3028
|
-
set({ editorSidebarWidth: width });
|
|
3029
|
-
localStorage.setItem('groove:editorSidebarWidth', String(width));
|
|
3030
|
-
},
|
|
3031
|
-
setEditorTheme(theme) {
|
|
3032
|
-
set({ editorTheme: theme });
|
|
3033
|
-
localStorage.setItem('groove:editorTheme', theme);
|
|
3034
|
-
},
|
|
3035
|
-
|
|
3036
|
-
updateFileContent(path, content) {
|
|
3037
|
-
set((s) => ({ editorFiles: { ...s.editorFiles, [path]: { ...s.editorFiles[path], content } } }));
|
|
3038
|
-
},
|
|
3039
|
-
|
|
3040
|
-
async saveFile(path) {
|
|
3041
|
-
const file = get().editorFiles[path];
|
|
3042
|
-
if (!file) return;
|
|
3043
|
-
try {
|
|
3044
|
-
await api.post('/files/write', { path, content: file.content });
|
|
3045
|
-
set((s) => ({
|
|
3046
|
-
editorFiles: { ...s.editorFiles, [path]: { ...s.editorFiles[path], originalContent: file.content } },
|
|
3047
|
-
editorChangedFiles: (() => { const c = { ...s.editorChangedFiles }; delete c[path]; return c; })(),
|
|
3048
|
-
editorRecentSaves: { ...s.editorRecentSaves, [path]: Date.now() },
|
|
3049
|
-
}));
|
|
3050
|
-
get().addToast('success', 'File saved');
|
|
3051
|
-
} catch (err) {
|
|
3052
|
-
get().addToast('error', 'Save failed', err.message);
|
|
3053
|
-
}
|
|
3054
|
-
},
|
|
3055
|
-
|
|
3056
|
-
async reloadFile(path) {
|
|
3057
|
-
try {
|
|
3058
|
-
const data = await api.get(`/files/read?path=${encodeURIComponent(path)}`);
|
|
3059
|
-
if (data.binary) return;
|
|
3060
|
-
set((s) => ({
|
|
3061
|
-
editorFiles: { ...s.editorFiles, [path]: { content: data.content, originalContent: data.content, language: data.language, loadedAt: Date.now() } },
|
|
3062
|
-
editorChangedFiles: (() => { const c = { ...s.editorChangedFiles }; delete c[path]; return c; })(),
|
|
3063
|
-
}));
|
|
3064
|
-
} catch { /* ignore */ }
|
|
3065
|
-
},
|
|
3066
|
-
|
|
3067
|
-
dismissFileChange(path) {
|
|
3068
|
-
set((s) => { const c = { ...s.editorChangedFiles }; delete c[path]; return { editorChangedFiles: c }; });
|
|
3069
|
-
},
|
|
3070
|
-
|
|
3071
|
-
async fetchTreeDir(dirPath) {
|
|
3072
|
-
try {
|
|
3073
|
-
const data = await api.get(`/files/tree?path=${encodeURIComponent(dirPath)}`);
|
|
3074
|
-
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: data.entries || [] } }));
|
|
3075
|
-
const ws = get().ws;
|
|
3076
|
-
if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watchdir', path: dirPath }));
|
|
3077
|
-
} catch (err) {
|
|
3078
|
-
console.error('[file-tree] fetchTreeDir failed for', dirPath, err.message);
|
|
3079
|
-
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: [] } }));
|
|
3080
|
-
}
|
|
3081
|
-
},
|
|
3082
|
-
|
|
3083
|
-
async createFile(relPath) {
|
|
3084
|
-
try {
|
|
3085
|
-
await api.post('/files/create', { path: relPath });
|
|
3086
|
-
const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
|
|
3087
|
-
await get().fetchTreeDir(parent);
|
|
3088
|
-
get().addToast('success', 'File created');
|
|
3089
|
-
return true;
|
|
3090
|
-
} catch (err) {
|
|
3091
|
-
get().addToast('error', 'Create failed', err.message);
|
|
3092
|
-
return false;
|
|
3093
|
-
}
|
|
3094
|
-
},
|
|
3095
|
-
|
|
3096
|
-
async createDir(relPath) {
|
|
3097
|
-
try {
|
|
3098
|
-
await api.post('/files/mkdir', { path: relPath });
|
|
3099
|
-
const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
|
|
3100
|
-
await get().fetchTreeDir(parent);
|
|
3101
|
-
get().addToast('success', 'Folder created');
|
|
3102
|
-
return true;
|
|
3103
|
-
} catch (err) {
|
|
3104
|
-
get().addToast('error', 'Create failed', err.message);
|
|
3105
|
-
return false;
|
|
3106
|
-
}
|
|
3107
|
-
},
|
|
3108
|
-
|
|
3109
|
-
async deleteFile(relPath) {
|
|
3110
|
-
try {
|
|
3111
|
-
await api.delete(`/files/delete?path=${encodeURIComponent(relPath)}`);
|
|
3112
|
-
if (get().editorOpenTabs.includes(relPath)) get().closeFile(relPath);
|
|
3113
|
-
const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
|
|
3114
|
-
await get().fetchTreeDir(parent);
|
|
3115
|
-
set((s) => { const cache = { ...s.editorTreeCache }; delete cache[relPath]; return { editorTreeCache: cache }; });
|
|
3116
|
-
get().addToast('success', 'Deleted');
|
|
3117
|
-
return true;
|
|
3118
|
-
} catch (err) {
|
|
3119
|
-
get().addToast('error', 'Delete failed', err.message);
|
|
3120
|
-
return false;
|
|
3121
|
-
}
|
|
3122
|
-
},
|
|
3123
|
-
|
|
3124
|
-
// ── Workspace Mode ────────────────────────────────────────
|
|
3125
|
-
|
|
3126
|
-
setWorkspaceMode(on) {
|
|
3127
|
-
set({ workspaceMode: on });
|
|
3128
|
-
localStorage.setItem('groove:workspaceMode', String(on));
|
|
3129
|
-
if (on) {
|
|
3130
|
-
const teamAgents = get().agents.filter((a) => a.teamId === get().activeTeamId);
|
|
3131
|
-
const current = get().workspaceAgentId;
|
|
3132
|
-
const belongsToTeam = current && teamAgents.some((a) => a.id === current);
|
|
3133
|
-
if (!belongsToTeam) {
|
|
3134
|
-
const selected = get().detailPanel?.type === 'agent' ? get().detailPanel.agentId : null;
|
|
3135
|
-
const selectedInTeam = selected && teamAgents.some((a) => a.id === selected);
|
|
3136
|
-
const running = teamAgents.find((a) => a.status === 'running');
|
|
3137
|
-
set({ workspaceAgentId: (selectedInTeam ? selected : null) || running?.id || teamAgents[0]?.id || null });
|
|
3138
|
-
}
|
|
3139
|
-
const agentId = get().workspaceAgentId;
|
|
3140
|
-
if (agentId) get().selectAgent(agentId);
|
|
3141
|
-
}
|
|
3142
|
-
},
|
|
3143
|
-
|
|
3144
|
-
setWorkspaceAgent(id) {
|
|
3145
|
-
set({ workspaceAgentId: id });
|
|
3146
|
-
if (id) get().selectAgent(id);
|
|
3147
|
-
},
|
|
3148
|
-
|
|
3149
|
-
captureSnapshot(path, content) {
|
|
3150
|
-
set((s) => {
|
|
3151
|
-
if (s.workspaceSnapshots[path]) return s;
|
|
3152
|
-
const next = { ...s.workspaceSnapshots, [path]: content };
|
|
3153
|
-
const keys = Object.keys(next);
|
|
3154
|
-
if (keys.length > 200) {
|
|
3155
|
-
delete next[keys[0]];
|
|
3156
|
-
}
|
|
3157
|
-
return { workspaceSnapshots: next };
|
|
3158
|
-
});
|
|
3159
|
-
},
|
|
3160
|
-
|
|
3161
|
-
async toggleReviewMode() {
|
|
3162
|
-
const st = get();
|
|
3163
|
-
if (st.workspaceReviewMode) {
|
|
3164
|
-
set({ workspaceReviewMode: false, workspaceReviewFiles: [] });
|
|
3165
|
-
return;
|
|
3166
|
-
}
|
|
3167
|
-
const agentId = st.workspaceAgentId;
|
|
3168
|
-
if (!agentId) return;
|
|
3169
|
-
try {
|
|
3170
|
-
const res = await api.get(`/agents/${agentId}/files-touched`);
|
|
3171
|
-
const touched = res.data || [];
|
|
3172
|
-
const files = touched
|
|
3173
|
-
.filter((f) => f.writes > 0)
|
|
3174
|
-
.map((f) => ({ path: f.path, status: 'pending', comment: '' }));
|
|
3175
|
-
set({ workspaceReviewMode: true, workspaceReviewFiles: files });
|
|
3176
|
-
} catch (err) {
|
|
3177
|
-
console.error('Failed to fetch touched files for review:', err);
|
|
3178
|
-
}
|
|
3179
|
-
},
|
|
3180
|
-
|
|
3181
|
-
approveFile(path) {
|
|
3182
|
-
set((s) => ({
|
|
3183
|
-
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
3184
|
-
f.path === path ? { ...f, status: 'approved' } : f,
|
|
3185
|
-
),
|
|
3186
|
-
}));
|
|
3187
|
-
},
|
|
3188
|
-
|
|
3189
|
-
rejectFile(path) {
|
|
3190
|
-
set((s) => ({
|
|
3191
|
-
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
3192
|
-
f.path === path ? { ...f, status: 'rejected' } : f,
|
|
3193
|
-
),
|
|
3194
|
-
}));
|
|
3195
|
-
},
|
|
3196
|
-
|
|
3197
|
-
commentFile(path, comment) {
|
|
3198
|
-
set((s) => ({
|
|
3199
|
-
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
3200
|
-
f.path === path ? { ...f, comment } : f,
|
|
3201
|
-
),
|
|
3202
|
-
}));
|
|
3203
|
-
},
|
|
3204
|
-
|
|
3205
|
-
// ── Editor (Cursor-style) ──────────────────────────────────
|
|
3206
|
-
|
|
3207
|
-
setEditorAgent(id) {
|
|
3208
|
-
set({ editorSelectedAgent: id });
|
|
3209
|
-
},
|
|
3210
|
-
|
|
3211
|
-
setEditorViewMode(mode) {
|
|
3212
|
-
set({ editorViewMode: mode });
|
|
3213
|
-
},
|
|
3214
|
-
|
|
3215
|
-
toggleAiPanel() {
|
|
3216
|
-
set((s) => {
|
|
3217
|
-
const open = !s.editorAiPanelOpen;
|
|
3218
|
-
return { editorAiPanelOpen: open };
|
|
3219
|
-
});
|
|
3220
|
-
},
|
|
3221
|
-
|
|
3222
|
-
setEditorAiPanelWidth(width) {
|
|
3223
|
-
set({ editorAiPanelWidth: width });
|
|
3224
|
-
localStorage.setItem('groove:editorAiPanelWidth', String(width));
|
|
3225
|
-
},
|
|
3226
|
-
|
|
3227
|
-
setEditorQuickSearchOpen(open) {
|
|
3228
|
-
set({ editorQuickSearchOpen: open });
|
|
3229
|
-
},
|
|
3230
|
-
|
|
3231
|
-
attachSnippet(snippet) {
|
|
3232
|
-
set({ editorPendingSnippet: snippet });
|
|
3233
|
-
if (!get().editorAiPanelOpen) {
|
|
3234
|
-
set({ editorAiPanelOpen: true });
|
|
3235
|
-
}
|
|
3236
|
-
},
|
|
3237
|
-
|
|
3238
|
-
clearSnippet() {
|
|
3239
|
-
set({ editorPendingSnippet: null });
|
|
3240
|
-
},
|
|
3241
|
-
|
|
3242
|
-
async sendCodeToAgent(agentId, instruction, filePath, lineStart, lineEnd, selectedCode) {
|
|
3243
|
-
if (!agentId) return;
|
|
3244
|
-
get().attachSnippet({
|
|
3245
|
-
type: 'code',
|
|
3246
|
-
instruction,
|
|
3247
|
-
filePath,
|
|
3248
|
-
lineStart,
|
|
3249
|
-
lineEnd,
|
|
3250
|
-
code: selectedCode,
|
|
3251
|
-
});
|
|
3252
|
-
},
|
|
3253
|
-
|
|
3254
|
-
async fetchGitStatus() {
|
|
3255
|
-
try {
|
|
3256
|
-
const data = await api.get('/files/git-status');
|
|
3257
|
-
set({ editorGitStatus: data });
|
|
3258
|
-
return data;
|
|
3259
|
-
} catch { return null; }
|
|
3260
|
-
},
|
|
3261
|
-
|
|
3262
|
-
async fetchGitBranch() {
|
|
3263
|
-
try {
|
|
3264
|
-
const data = await api.get('/files/git-branch');
|
|
3265
|
-
set({ editorGitBranch: data });
|
|
3266
|
-
return data;
|
|
3267
|
-
} catch { return null; }
|
|
3268
|
-
},
|
|
3269
|
-
|
|
3270
|
-
async fetchGitDiff(path) {
|
|
3271
|
-
try {
|
|
3272
|
-
const url = path ? `/files/git-diff?path=${encodeURIComponent(path)}` : '/files/git-diff';
|
|
3273
|
-
const data = await api.get(url);
|
|
3274
|
-
set({ editorGitDiff: data });
|
|
3275
|
-
return data;
|
|
3276
|
-
} catch { return null; }
|
|
3277
|
-
},
|
|
3278
|
-
|
|
3279
|
-
// ── Federation ────────────────────────────────────────────
|
|
3280
|
-
|
|
3281
|
-
async fetchFederationStatus() {
|
|
3282
|
-
try {
|
|
3283
|
-
const data = await api.get('/federation');
|
|
3284
|
-
set((s) => ({
|
|
3285
|
-
federation: {
|
|
3286
|
-
...s.federation,
|
|
3287
|
-
peers: data.peers || [],
|
|
3288
|
-
whitelist: data.whitelist || [],
|
|
3289
|
-
connections: data.connections || [],
|
|
3290
|
-
ambassadors: data.ambassadors?.ambassadors || data.ambassadors || [],
|
|
3291
|
-
},
|
|
3292
|
-
}));
|
|
3293
|
-
return data;
|
|
3294
|
-
} catch { return null; }
|
|
3295
|
-
},
|
|
3296
|
-
|
|
3297
|
-
async addToWhitelist(ip, port = 31415, name) {
|
|
3298
|
-
try {
|
|
3299
|
-
await api.post('/federation/whitelist', { ip, port, ...(name && { name }) });
|
|
3300
|
-
get().addToast('success', `Added ${ip} to whitelist`);
|
|
3301
|
-
get().fetchFederationStatus();
|
|
3302
|
-
} catch (err) {
|
|
3303
|
-
get().addToast('error', 'Whitelist failed', err.message);
|
|
3304
|
-
throw err;
|
|
3305
|
-
}
|
|
3306
|
-
},
|
|
3307
|
-
|
|
3308
|
-
async removeFromWhitelist(ip) {
|
|
3309
|
-
try {
|
|
3310
|
-
await api.delete(`/federation/whitelist/${encodeURIComponent(ip)}`);
|
|
3311
|
-
get().addToast('info', `Removed ${ip}`);
|
|
3312
|
-
get().fetchFederationStatus();
|
|
3313
|
-
} catch (err) {
|
|
3314
|
-
get().addToast('error', 'Remove failed', err.message);
|
|
3315
|
-
}
|
|
3316
|
-
},
|
|
3317
|
-
|
|
3318
|
-
setSelectedPeer(peerId) {
|
|
3319
|
-
set((s) => ({ federation: { ...s.federation, selectedPeerId: peerId } }));
|
|
3320
|
-
},
|
|
3321
|
-
|
|
3322
|
-
async fetchPouchLog(peerId) {
|
|
3323
|
-
try {
|
|
3324
|
-
const data = await api.get(`/federation/pouch/log${peerId ? `?peerId=${encodeURIComponent(peerId)}` : ''}`);
|
|
3325
|
-
set((s) => ({ federation: { ...s.federation, pouchLog: data || [] } }));
|
|
3326
|
-
} catch { /* ignore */ }
|
|
3327
|
-
},
|
|
3328
|
-
|
|
3329
|
-
async sendPouch(peerId, contract) {
|
|
3330
|
-
try {
|
|
3331
|
-
const result = await api.post('/federation/pouch/send', { peerId, contract });
|
|
3332
|
-
get().addToast('success', 'Pouch sent');
|
|
3333
|
-
return result;
|
|
3334
|
-
} catch (err) {
|
|
3335
|
-
get().addToast('error', 'Pouch send failed', err.message);
|
|
3336
|
-
throw err;
|
|
3337
|
-
}
|
|
3338
|
-
},
|
|
3339
|
-
|
|
3340
|
-
async disconnectPeer(peerId) {
|
|
3341
|
-
try {
|
|
3342
|
-
await api.delete(`/federation/peers/${encodeURIComponent(peerId)}`);
|
|
3343
|
-
get().addToast('info', 'Peer disconnected');
|
|
3344
|
-
get().fetchFederationStatus();
|
|
3345
|
-
} catch (err) {
|
|
3346
|
-
get().addToast('error', 'Disconnect failed', err.message);
|
|
3347
|
-
}
|
|
3348
|
-
},
|
|
3349
|
-
|
|
3350
|
-
// ── Training Data ─────────────────────────────────────────
|
|
3351
|
-
|
|
3352
|
-
async setTrainingOptIn(enabled) {
|
|
3353
|
-
try {
|
|
3354
|
-
await api.post('/training/opt-in', { enabled });
|
|
3355
|
-
set({ trainingOptIn: enabled, dataSharingModalOpen: false });
|
|
3356
|
-
if (!enabled) set({ trainingStats: null });
|
|
3357
|
-
} catch (e) {
|
|
3358
|
-
get().addToast('error', 'Failed to update training preference', e.body?.detail || e.message);
|
|
3359
|
-
}
|
|
3360
|
-
},
|
|
3361
|
-
|
|
3362
|
-
async fetchTrainingStatus() {
|
|
3363
|
-
try {
|
|
3364
|
-
const data = await api.get('/training/status');
|
|
3365
|
-
set({ trainingOptIn: data.optedIn, trainingStats: data });
|
|
3366
|
-
} catch { /* endpoint may not exist on older daemons */ }
|
|
3367
|
-
},
|
|
3368
|
-
|
|
3369
|
-
async dismissDataSharingModal(permanent) {
|
|
3370
|
-
if (permanent) {
|
|
3371
|
-
try { await api.patch('/config', { dataSharingDismissed: true }); } catch {}
|
|
3372
|
-
set({ dataSharingDismissed: true, dataSharingModalOpen: false });
|
|
3373
|
-
} else {
|
|
3374
|
-
set({ dataSharingModalOpen: false });
|
|
3375
|
-
}
|
|
3376
|
-
},
|
|
3377
|
-
|
|
3378
|
-
// ── Network (Early Access) ────────────────────────────────
|
|
3379
|
-
|
|
3380
|
-
async fetchBetaStatus() {
|
|
3381
|
-
try {
|
|
3382
|
-
const data = await api.get('/beta/status');
|
|
3383
|
-
set({ networkUnlocked: !!data?.unlocked });
|
|
3384
|
-
} catch { /* endpoint may not exist yet */ }
|
|
3385
|
-
},
|
|
3386
|
-
|
|
3387
|
-
async activateBeta(code) {
|
|
3388
|
-
const data = await api.post('/beta/activate', { code });
|
|
3389
|
-
if (!data?.unlocked) {
|
|
3390
|
-
throw new Error(data?.message || 'Invalid invite code');
|
|
3391
|
-
}
|
|
3392
|
-
set({ networkUnlocked: true });
|
|
3393
|
-
return data;
|
|
3394
|
-
},
|
|
3395
|
-
|
|
3396
|
-
async deactivateBeta() {
|
|
3397
|
-
try {
|
|
3398
|
-
await api.post('/beta/deactivate');
|
|
3399
|
-
set({
|
|
3400
|
-
networkUnlocked: false,
|
|
3401
|
-
activeView: get().activeView === 'network' ? 'agents' : get().activeView,
|
|
3402
|
-
});
|
|
3403
|
-
} catch (err) {
|
|
3404
|
-
get().addToast('error', 'Deactivate failed', err.message);
|
|
3405
|
-
throw err;
|
|
3406
|
-
}
|
|
3407
|
-
},
|
|
3408
|
-
|
|
3409
|
-
async fetchNetworkNodeStatus() {
|
|
3410
|
-
try {
|
|
3411
|
-
const data = await api.get('/network/node/status');
|
|
3412
|
-
const update = { networkNode: { ...get().networkNode, ...(data || {}) } };
|
|
3413
|
-
if (data && typeof data.installed === 'boolean') {
|
|
3414
|
-
update.networkInstalled = data.installed;
|
|
3415
|
-
}
|
|
3416
|
-
set(update);
|
|
3417
|
-
return data;
|
|
3418
|
-
} catch { return null; }
|
|
3419
|
-
},
|
|
3420
|
-
|
|
3421
|
-
async fetchNetworkInstallStatus() {
|
|
3422
|
-
try {
|
|
3423
|
-
const data = await api.get('/network/install/status');
|
|
3424
|
-
if (data && typeof data.installed === 'boolean') {
|
|
3425
|
-
set({ networkInstalled: data.installed });
|
|
3426
|
-
}
|
|
3427
|
-
return data;
|
|
3428
|
-
} catch { return null; }
|
|
3429
|
-
},
|
|
3430
|
-
|
|
3431
|
-
async installNetworkPackage() {
|
|
3432
|
-
set({
|
|
3433
|
-
networkInstallProgress: {
|
|
3434
|
-
installing: true,
|
|
3435
|
-
step: 'starting',
|
|
3436
|
-
message: 'Starting install…',
|
|
3437
|
-
percent: 0,
|
|
3438
|
-
error: null,
|
|
3439
|
-
},
|
|
3440
|
-
});
|
|
3441
|
-
try {
|
|
3442
|
-
await api.post('/network/install');
|
|
3443
|
-
} catch (err) {
|
|
3444
|
-
set({
|
|
3445
|
-
networkInstallProgress: {
|
|
3446
|
-
installing: false,
|
|
3447
|
-
step: 'error',
|
|
3448
|
-
message: err.message,
|
|
3449
|
-
percent: 0,
|
|
3450
|
-
error: err.message,
|
|
3451
|
-
},
|
|
3452
|
-
});
|
|
3453
|
-
get().addToast('error', 'Install failed', err.message);
|
|
3454
|
-
}
|
|
3455
|
-
},
|
|
3456
|
-
|
|
3457
|
-
async uninstallNetworkPackage() {
|
|
3458
|
-
try {
|
|
3459
|
-
await api.post('/network/uninstall');
|
|
3460
|
-
set({
|
|
3461
|
-
networkInstalled: false,
|
|
3462
|
-
networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
|
|
3463
|
-
networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
|
|
3464
|
-
});
|
|
3465
|
-
get().addToast('success', 'Network package uninstalled');
|
|
3466
|
-
} catch (err) {
|
|
3467
|
-
get().addToast('error', 'Uninstall failed', err.message);
|
|
3468
|
-
throw err;
|
|
3469
|
-
}
|
|
3470
|
-
},
|
|
3471
|
-
|
|
3472
|
-
async fetchNetworkStatus() {
|
|
3473
|
-
try {
|
|
3474
|
-
const data = await api.get('/network/status');
|
|
3475
|
-
const update = {
|
|
3476
|
-
networkStatus: { ...get().networkStatus, ...(data || {}) },
|
|
3477
|
-
networkStatusReachable: true,
|
|
3478
|
-
};
|
|
3479
|
-
if (data?.compute) {
|
|
3480
|
-
const c = data.compute;
|
|
3481
|
-
update.networkCompute = {
|
|
3482
|
-
totalRamMb: c.totalRamMb ?? c.total_ram_mb ?? 0,
|
|
3483
|
-
totalVramMb: c.totalVramMb ?? c.total_vram_mb ?? 0,
|
|
3484
|
-
totalCpuCores: c.totalCpuCores ?? c.total_cpu_cores ?? 0,
|
|
3485
|
-
totalBandwidthMbps: c.totalBandwidthMbps ?? c.total_bandwidth_mbps ?? 0,
|
|
3486
|
-
activeNodes: c.activeNodes ?? c.active_nodes ?? 0,
|
|
3487
|
-
totalNodes: c.totalNodes ?? c.total_nodes ?? 0,
|
|
3488
|
-
avgLoad: c.avgLoad ?? c.avg_load ?? 0,
|
|
3489
|
-
};
|
|
3490
|
-
} else if (Array.isArray(data?.nodes) && data.nodes.length > 0) {
|
|
3491
|
-
const nodes = data.nodes;
|
|
3492
|
-
const active = nodes.filter((n) => n.status === 'active');
|
|
3493
|
-
update.networkCompute = {
|
|
3494
|
-
totalRamMb: nodes.reduce((s, n) => s + (n.ram_mb || 0), 0),
|
|
3495
|
-
totalVramMb: nodes.reduce((s, n) => s + (n.vram_mb || 0), 0),
|
|
3496
|
-
totalCpuCores: nodes.reduce((s, n) => s + (n.cpu_cores || 0), 0),
|
|
3497
|
-
totalBandwidthMbps: nodes.reduce((s, n) => s + (n.bandwidth_mbps || 0), 0),
|
|
3498
|
-
activeNodes: active.length,
|
|
3499
|
-
totalNodes: nodes.length,
|
|
3500
|
-
avgLoad: active.length > 0 ? active.reduce((s, n) => s + (n.load || 0), 0) / active.length : 0,
|
|
3501
|
-
};
|
|
3502
|
-
}
|
|
3503
|
-
set(update);
|
|
3504
|
-
|
|
3505
|
-
// Push snapshot for activity chart
|
|
3506
|
-
if (data) {
|
|
3507
|
-
const ownId = get().networkNode.nodeId;
|
|
3508
|
-
const nodes = data.nodes || [];
|
|
3509
|
-
const ownNode = ownId ? nodes.find((n) => (n.node_id || n.nodeId) === ownId) : null;
|
|
3510
|
-
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
3511
|
-
const snap = {
|
|
3512
|
-
t: Date.now(),
|
|
3513
|
-
globalSessions: data.activeSessions || 0,
|
|
3514
|
-
mySessions: ownNode?.active_sessions ?? ownNode?.sessions ?? 0,
|
|
3515
|
-
nodeCount: activeNodes.length,
|
|
3516
|
-
avgLoad: activeNodes.length > 0 ? activeNodes.reduce((s, n) => s + (n.load || 0), 0) / activeNodes.length : 0,
|
|
3517
|
-
myLoad: ownNode?.load ?? 0,
|
|
3518
|
-
totalVramMb: nodes.reduce((s, n) => s + (n.vram_mb || 0), 0),
|
|
3519
|
-
totalRamMb: nodes.reduce((s, n) => s + (n.ram_mb || 0), 0),
|
|
3520
|
-
};
|
|
3521
|
-
let snapshots = [...get().networkSnapshots, snap];
|
|
3522
|
-
if (snapshots.length > 100) snapshots = snapshots.slice(-100);
|
|
3523
|
-
set({ networkSnapshots: snapshots });
|
|
3524
|
-
}
|
|
3525
|
-
|
|
3526
|
-
return data;
|
|
3527
|
-
} catch {
|
|
3528
|
-
set({ networkStatusReachable: false });
|
|
3529
|
-
return null;
|
|
3530
|
-
}
|
|
3531
|
-
},
|
|
3532
|
-
|
|
3533
|
-
async checkNetworkUpdate() {
|
|
3534
|
-
try {
|
|
3535
|
-
const data = await api.get('/network/update/check');
|
|
3536
|
-
if (!data) return null;
|
|
3537
|
-
set({
|
|
3538
|
-
networkVersion: {
|
|
3539
|
-
installed: data.installed ?? null,
|
|
3540
|
-
latest: data.latest ?? null,
|
|
3541
|
-
updateAvailable: !!data.updateAvailable,
|
|
3542
|
-
},
|
|
3543
|
-
});
|
|
3544
|
-
return data;
|
|
3545
|
-
} catch { return null; }
|
|
3546
|
-
},
|
|
3547
|
-
|
|
3548
|
-
async updateNetworkPackage() {
|
|
3549
|
-
set({
|
|
3550
|
-
networkUpdateProgress: {
|
|
3551
|
-
updating: true,
|
|
3552
|
-
step: 'starting',
|
|
3553
|
-
message: 'Starting update…',
|
|
3554
|
-
percent: 0,
|
|
3555
|
-
error: null,
|
|
3556
|
-
},
|
|
3557
|
-
});
|
|
3558
|
-
try {
|
|
3559
|
-
await api.post('/network/update');
|
|
3560
|
-
} catch (err) {
|
|
3561
|
-
set({
|
|
3562
|
-
networkUpdateProgress: {
|
|
3563
|
-
updating: false,
|
|
3564
|
-
step: 'error',
|
|
3565
|
-
message: err.message,
|
|
3566
|
-
percent: 0,
|
|
3567
|
-
error: err.message,
|
|
3568
|
-
},
|
|
3569
|
-
});
|
|
3570
|
-
get().addToast('error', 'Update failed', err.message);
|
|
3571
|
-
}
|
|
3572
|
-
},
|
|
3573
|
-
|
|
3574
|
-
async startNetworkNode() {
|
|
3575
|
-
set({ networkNode: { ...get().networkNode, status: 'connecting' } });
|
|
3576
|
-
try {
|
|
3577
|
-
const data = await api.post('/network/node/start');
|
|
3578
|
-
set({ networkNode: { ...get().networkNode, active: true, ...(data || {}) } });
|
|
3579
|
-
get().addToast('success', 'Node started', 'Connecting to the Groove network');
|
|
3580
|
-
return data;
|
|
3581
|
-
} catch (err) {
|
|
3582
|
-
set({ networkNode: { ...get().networkNode, status: 'disconnected', active: false } });
|
|
3583
|
-
get().addToast('error', 'Node start failed', err.message);
|
|
3584
|
-
throw err;
|
|
3585
|
-
}
|
|
3586
|
-
},
|
|
3587
|
-
|
|
3588
|
-
async stopNetworkNode() {
|
|
3589
|
-
try {
|
|
3590
|
-
await api.post('/network/node/stop');
|
|
3591
|
-
set({ networkNode: { ...get().networkNode, active: false, status: 'disconnected' } });
|
|
3592
|
-
get().addToast('info', 'Node stopped');
|
|
3593
|
-
} catch (err) {
|
|
3594
|
-
get().addToast('error', 'Node stop failed', err.message);
|
|
3595
|
-
throw err;
|
|
3596
|
-
}
|
|
3597
|
-
},
|
|
3598
|
-
|
|
3599
|
-
async fetchNetworkWallet() {
|
|
3600
|
-
return get().networkWallet;
|
|
3601
|
-
},
|
|
3602
|
-
async fetchNetworkEarnings() {
|
|
3603
|
-
return get().networkEarnings;
|
|
3604
|
-
},
|
|
3605
|
-
|
|
3606
|
-
async fetchNetworkBenchmarks() {
|
|
3607
|
-
try {
|
|
3608
|
-
const data = await api.get('/network/benchmarks');
|
|
3609
|
-
if (Array.isArray(data)) set({ networkBenchmarks: data.slice(-100) });
|
|
3610
|
-
return data;
|
|
3611
|
-
} catch { return null; }
|
|
3612
|
-
},
|
|
3613
|
-
|
|
3614
|
-
async fetchNetworkTraces() {
|
|
3615
|
-
try {
|
|
3616
|
-
const data = await api.get('/network/traces');
|
|
3617
|
-
if (Array.isArray(data)) set({ networkTraces: data });
|
|
3618
|
-
return data;
|
|
3619
|
-
} catch { return null; }
|
|
3620
|
-
},
|
|
3621
|
-
|
|
3622
|
-
async fetchNetworkTrace(filename) {
|
|
3623
|
-
try {
|
|
3624
|
-
return await api.get(`/network/traces/${encodeURIComponent(filename)}`);
|
|
3625
|
-
} catch { return null; }
|
|
3626
|
-
},
|
|
3627
|
-
|
|
3628
|
-
async fetchLiveTrace(offset = 0) {
|
|
3629
|
-
try {
|
|
3630
|
-
return await api.get(`/network/traces/live?offset=${offset}`);
|
|
3631
|
-
} catch { return null; }
|
|
3632
|
-
},
|
|
3633
|
-
|
|
3634
|
-
// ── Model Lab Actions ──────────────────────────────────────
|
|
3635
|
-
|
|
3636
|
-
setLabParameter(key, value) {
|
|
3637
|
-
const params = { ...get().labParameters, [key]: value };
|
|
3638
|
-
set({ labParameters: params });
|
|
3639
|
-
persistJSON('groove:labParameters', params);
|
|
3640
|
-
},
|
|
3641
|
-
|
|
3642
|
-
setLabSystemPrompt(text) {
|
|
3643
|
-
set({ labSystemPrompt: text });
|
|
3644
|
-
localStorage.setItem('groove:labSystemPrompt', text);
|
|
3645
|
-
},
|
|
3646
|
-
|
|
3647
|
-
async fetchLabRuntimes() {
|
|
3648
|
-
try {
|
|
3649
|
-
const raw = await api.get('/lab/runtimes');
|
|
3650
|
-
const data = raw.map((rt) => ({
|
|
3651
|
-
...rt,
|
|
3652
|
-
status: rt.online === true ? 'connected' : rt.online === false ? 'error' : rt.status,
|
|
3653
|
-
}));
|
|
3654
|
-
set({ labRuntimes: data });
|
|
3655
|
-
persistJSON('groove:labRuntimes', data);
|
|
3656
|
-
if (data.length > 0 && !get().labActiveRuntime) {
|
|
3657
|
-
get().setLabActiveRuntime(data[0].id);
|
|
3658
|
-
} else if (get().labActiveRuntime) {
|
|
3659
|
-
get().fetchLabModels(get().labActiveRuntime);
|
|
3660
|
-
}
|
|
3661
|
-
} catch { /* backend may not have lab endpoints yet */ }
|
|
3662
|
-
},
|
|
3663
|
-
|
|
3664
|
-
async fetchLabLocalModels() {
|
|
3665
|
-
try {
|
|
3666
|
-
const data = await api.get('/lab/local-models');
|
|
3667
|
-
set({ labLocalModels: data });
|
|
3668
|
-
} catch { set({ labLocalModels: [] }); }
|
|
3669
|
-
},
|
|
3670
|
-
|
|
3671
|
-
async checkLlamaStatus() {
|
|
3672
|
-
try {
|
|
3673
|
-
const data = await api.get('/llama/status');
|
|
3674
|
-
set({ labLlamaInstalled: !!data.installed });
|
|
3675
|
-
} catch { set({ labLlamaInstalled: false }); }
|
|
3676
|
-
},
|
|
3677
|
-
|
|
3678
|
-
async launchLocalModel(modelId) {
|
|
3679
|
-
set({ labLaunching: modelId, labLaunchPhase: 'starting', labLaunchError: null });
|
|
3680
|
-
try {
|
|
3681
|
-
const result = await api.post('/lab/launch-local', { modelId });
|
|
3682
|
-
const runtimes = await api.get('/lab/runtimes');
|
|
3683
|
-
set({ labRuntimes: runtimes });
|
|
3684
|
-
persistJSON('groove:labRuntimes', runtimes);
|
|
3685
|
-
get().setLabActiveRuntime(result.runtime.id);
|
|
3686
|
-
set({ labActiveModel: result.model, labLaunching: null, labLaunchPhase: 'ready' });
|
|
3687
|
-
get().addToast('success', `Launched ${result.model}`);
|
|
3688
|
-
setTimeout(() => { if (get().labLaunchPhase === 'ready') set({ labLaunchPhase: null }); }, 3000);
|
|
3689
|
-
return result;
|
|
3690
|
-
} catch (err) {
|
|
3691
|
-
set({ labLaunching: null, labLaunchPhase: 'error', labLaunchError: err.message });
|
|
3692
|
-
get().addToast('error', 'Failed to launch model', err.message);
|
|
3693
|
-
throw err;
|
|
3694
|
-
}
|
|
3695
|
-
},
|
|
3696
|
-
|
|
3697
|
-
async addLabRuntime(runtime) {
|
|
3698
|
-
try {
|
|
3699
|
-
const created = await api.post('/lab/runtimes', runtime);
|
|
3700
|
-
const runtimes = [...get().labRuntimes, created];
|
|
3701
|
-
set({ labRuntimes: runtimes });
|
|
3702
|
-
persistJSON('groove:labRuntimes', runtimes);
|
|
3703
|
-
get().setLabActiveRuntime(created.id);
|
|
3704
|
-
get().addToast('success', `Runtime "${runtime.name}" added`);
|
|
3705
|
-
return created;
|
|
3706
|
-
} catch (err) {
|
|
3707
|
-
get().addToast('error', 'Failed to add runtime', err.message);
|
|
3708
|
-
throw err;
|
|
3709
|
-
}
|
|
3710
|
-
},
|
|
3711
|
-
|
|
3712
|
-
async removeLabRuntime(id) {
|
|
3713
|
-
try {
|
|
3714
|
-
await api.delete(`/lab/runtimes/${id}`);
|
|
3715
|
-
const runtimes = get().labRuntimes.filter((r) => r.id !== id);
|
|
3716
|
-
const active = get().labActiveRuntime === id ? null : get().labActiveRuntime;
|
|
3717
|
-
set({ labRuntimes: runtimes, labActiveRuntime: active, labModels: active ? get().labModels : [] });
|
|
3718
|
-
persistJSON('groove:labRuntimes', runtimes);
|
|
3719
|
-
get().addToast('success', 'Runtime removed');
|
|
3720
|
-
} catch (err) {
|
|
3721
|
-
get().addToast('error', 'Failed to remove runtime', err.message);
|
|
3722
|
-
}
|
|
3723
|
-
},
|
|
3724
|
-
|
|
3725
|
-
async testLabRuntime(id) {
|
|
3726
|
-
try {
|
|
3727
|
-
const result = await api.post(`/lab/runtimes/${id}/test`);
|
|
3728
|
-
const runtimes = get().labRuntimes.map((r) =>
|
|
3729
|
-
r.id === id ? { ...r, status: result.ok ? 'connected' : 'error', latency: result.latency } : r,
|
|
3730
|
-
);
|
|
3731
|
-
const updates = { labRuntimes: runtimes };
|
|
3732
|
-
if (result.ok && result.models && get().labActiveRuntime === id) {
|
|
3733
|
-
updates.labModels = result.models;
|
|
3734
|
-
}
|
|
3735
|
-
set(updates);
|
|
3736
|
-
persistJSON('groove:labRuntimes', runtimes);
|
|
3737
|
-
return result;
|
|
3738
|
-
} catch (err) {
|
|
3739
|
-
const runtimes = get().labRuntimes.map((r) =>
|
|
3740
|
-
r.id === id ? { ...r, status: 'error' } : r,
|
|
3741
|
-
);
|
|
3742
|
-
set({ labRuntimes: runtimes });
|
|
3743
|
-
persistJSON('groove:labRuntimes', runtimes);
|
|
3744
|
-
return { ok: false, error: err.message };
|
|
3745
|
-
}
|
|
3746
|
-
},
|
|
3747
|
-
|
|
3748
|
-
setLabActiveRuntime(id) {
|
|
3749
|
-
set({ labActiveRuntime: id, labModels: [], labActiveModel: null });
|
|
3750
|
-
if (id) get().fetchLabModels(id);
|
|
3751
|
-
},
|
|
3752
|
-
|
|
3753
|
-
setLabActiveModel(model) {
|
|
3754
|
-
set({ labActiveModel: model });
|
|
3755
|
-
},
|
|
3756
|
-
|
|
3757
|
-
async fetchLabModels(runtimeId) {
|
|
3758
|
-
try {
|
|
3759
|
-
const data = await api.get(`/lab/runtimes/${runtimeId}/models`);
|
|
3760
|
-
set({ labModels: data });
|
|
3761
|
-
} catch { set({ labModels: [] }); }
|
|
3762
|
-
},
|
|
3763
|
-
|
|
3764
|
-
newLabSession() {
|
|
3765
|
-
const id = `lab-${Date.now()}`;
|
|
3766
|
-
const session = { id, messages: [], createdAt: Date.now() };
|
|
3767
|
-
set((s) => ({
|
|
3768
|
-
labSessions: [session, ...s.labSessions],
|
|
3769
|
-
labActiveSession: id,
|
|
3770
|
-
labMetrics: { ttft: null, tokensPerSec: null, tokensPerSecHistory: [], memory: null, totalTokens: 0, generationTime: null },
|
|
3771
|
-
}));
|
|
3772
|
-
return id;
|
|
3773
|
-
},
|
|
3774
|
-
|
|
3775
|
-
loadLabSession(id) {
|
|
3776
|
-
set({ labActiveSession: id });
|
|
3777
|
-
},
|
|
3778
|
-
|
|
3779
|
-
async sendLabMessage(text) {
|
|
3780
|
-
const st = get();
|
|
3781
|
-
if (st.labStreaming) return;
|
|
3782
|
-
let sessionId = st.labActiveSession;
|
|
3783
|
-
if (!sessionId) sessionId = get().newLabSession();
|
|
3784
|
-
|
|
3785
|
-
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
3786
|
-
set((s) => {
|
|
3787
|
-
const sessions = s.labSessions.map((sess) =>
|
|
3788
|
-
sess.id === sessionId ? { ...sess, messages: [...sess.messages, userMsg] } : sess,
|
|
3789
|
-
);
|
|
3790
|
-
return { labSessions: sessions, labStreaming: true };
|
|
3791
|
-
});
|
|
3792
|
-
|
|
3793
|
-
const assistantMsg = { role: 'assistant', content: '', timestamp: Date.now(), metrics: null };
|
|
3794
|
-
set((s) => {
|
|
3795
|
-
const sessions = s.labSessions.map((sess) =>
|
|
3796
|
-
sess.id === sessionId ? { ...sess, messages: [...sess.messages, assistantMsg] } : sess,
|
|
3797
|
-
);
|
|
3798
|
-
return { labSessions: sessions };
|
|
3799
|
-
});
|
|
3800
|
-
|
|
3801
|
-
const abortController = new AbortController();
|
|
3802
|
-
set({ labAbortController: abortController });
|
|
3803
|
-
|
|
3804
|
-
const startTime = performance.now();
|
|
3805
|
-
let firstTokenTime = null;
|
|
3806
|
-
let tokenCount = 0;
|
|
3807
|
-
|
|
3808
|
-
try {
|
|
3809
|
-
const p = st.labParameters;
|
|
3810
|
-
const parameters = {};
|
|
3811
|
-
if (p.temperature !== undefined) parameters.temperature = p.temperature;
|
|
3812
|
-
if (p.topP !== undefined) parameters.top_p = p.topP;
|
|
3813
|
-
if (p.topK !== undefined) parameters.top_k = p.topK;
|
|
3814
|
-
if (p.repeatPenalty !== undefined) parameters.repeat_penalty = p.repeatPenalty;
|
|
3815
|
-
if (p.maxTokens !== undefined) parameters.max_tokens = p.maxTokens;
|
|
3816
|
-
if (p.frequencyPenalty !== undefined) parameters.frequency_penalty = p.frequencyPenalty;
|
|
3817
|
-
if (p.presencePenalty !== undefined) parameters.presence_penalty = p.presencePenalty;
|
|
3818
|
-
|
|
3819
|
-
const messages = [];
|
|
3820
|
-
if (st.labSystemPrompt) messages.push({ role: 'system', content: st.labSystemPrompt });
|
|
3821
|
-
const sessionMsgs = get().labSessions.find((s) => s.id === sessionId)?.messages || [];
|
|
3822
|
-
for (const m of sessionMsgs) {
|
|
3823
|
-
if (m.role === 'assistant' && !m.content) continue;
|
|
3824
|
-
messages.push({ role: m.role, content: m.content });
|
|
3825
|
-
}
|
|
3826
|
-
|
|
3827
|
-
const body = {
|
|
3828
|
-
runtimeId: st.labActiveRuntime,
|
|
3829
|
-
model: st.labActiveModel,
|
|
3830
|
-
messages,
|
|
3831
|
-
parameters,
|
|
3832
|
-
sessionId,
|
|
3833
|
-
};
|
|
3834
|
-
|
|
3835
|
-
const res = await fetch('/api/lab/inference', {
|
|
3836
|
-
method: 'POST',
|
|
3837
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3838
|
-
body: JSON.stringify(body),
|
|
3839
|
-
signal: abortController.signal,
|
|
3840
|
-
});
|
|
3841
|
-
|
|
3842
|
-
if (!res.ok) {
|
|
3843
|
-
let errMsg;
|
|
3844
|
-
try { errMsg = (await res.json()).error || `HTTP ${res.status}`; } catch { errMsg = `HTTP ${res.status}`; }
|
|
3845
|
-
throw new Error(errMsg);
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
const reader = res.body.getReader();
|
|
3849
|
-
const decoder = new TextDecoder();
|
|
3850
|
-
let buffer = '';
|
|
3851
|
-
let fullContent = '';
|
|
3852
|
-
let fullReasoning = '';
|
|
3853
|
-
|
|
3854
|
-
while (true) {
|
|
3855
|
-
const { done, value } = await reader.read();
|
|
3856
|
-
if (done) break;
|
|
3857
|
-
buffer += decoder.decode(value, { stream: true });
|
|
3858
|
-
const lines = buffer.split('\n');
|
|
3859
|
-
buffer = lines.pop() || '';
|
|
3860
|
-
|
|
3861
|
-
for (const line of lines) {
|
|
3862
|
-
if (!line.startsWith('data: ')) continue;
|
|
3863
|
-
const payload = line.slice(6);
|
|
3864
|
-
if (payload === '[DONE]') continue;
|
|
3865
|
-
try {
|
|
3866
|
-
const parsed = JSON.parse(payload);
|
|
3867
|
-
|
|
3868
|
-
// Support both raw OpenAI format (piped) and legacy wrapper format
|
|
3869
|
-
const delta = parsed.choices?.[0]?.delta;
|
|
3870
|
-
const reasoningText = delta?.reasoning_content || (parsed.type === 'reasoning' ? parsed.content : null);
|
|
3871
|
-
const contentText = delta?.content || (parsed.type === 'token' ? parsed.content : null);
|
|
3872
|
-
|
|
3873
|
-
if (reasoningText) {
|
|
3874
|
-
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
3875
|
-
tokenCount++;
|
|
3876
|
-
fullReasoning += reasoningText;
|
|
3877
|
-
set((s) => {
|
|
3878
|
-
const sessions = s.labSessions.map((sess) => {
|
|
3879
|
-
if (sess.id !== sessionId) return sess;
|
|
3880
|
-
const msgs = [...sess.messages];
|
|
3881
|
-
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], reasoning: fullReasoning };
|
|
3882
|
-
return { ...sess, messages: msgs };
|
|
3883
|
-
});
|
|
3884
|
-
return { labSessions: sessions };
|
|
3885
|
-
});
|
|
3886
|
-
}
|
|
3887
|
-
if (contentText) {
|
|
3888
|
-
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
3889
|
-
tokenCount++;
|
|
3890
|
-
fullContent += contentText;
|
|
3891
|
-
set((s) => {
|
|
3892
|
-
const sessions = s.labSessions.map((sess) => {
|
|
3893
|
-
if (sess.id !== sessionId) return sess;
|
|
3894
|
-
const msgs = [...sess.messages];
|
|
3895
|
-
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: fullContent };
|
|
3896
|
-
return { ...sess, messages: msgs };
|
|
3897
|
-
});
|
|
3898
|
-
return { labSessions: sessions };
|
|
3899
|
-
});
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
// Handle done event (legacy wrapper) or finish_reason (raw OpenAI)
|
|
3903
|
-
if (parsed.type === 'done' && parsed.metrics) {
|
|
3904
|
-
const elapsed = performance.now() - startTime;
|
|
3905
|
-
const ttft = firstTokenTime ? firstTokenTime - startTime : null;
|
|
3906
|
-
const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
|
|
3907
|
-
const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...parsed.metrics };
|
|
3908
|
-
|
|
3909
|
-
set((s) => {
|
|
3910
|
-
const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
|
|
3911
|
-
const sessions = s.labSessions.map((sess) => {
|
|
3912
|
-
if (sess.id !== sessionId) return sess;
|
|
3913
|
-
const msgs = [...sess.messages];
|
|
3914
|
-
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], metrics: msgMetrics };
|
|
3915
|
-
return { ...sess, messages: msgs };
|
|
3916
|
-
});
|
|
3917
|
-
return {
|
|
3918
|
-
labSessions: sessions,
|
|
3919
|
-
labMetrics: {
|
|
3920
|
-
ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist,
|
|
3921
|
-
memory: parsed.metrics.memoryUsage || s.labMetrics.memory,
|
|
3922
|
-
totalTokens: s.labMetrics.totalTokens + (parsed.metrics.totalTokens || tokenCount),
|
|
3923
|
-
generationTime: parsed.metrics.generationTime || elapsed,
|
|
3924
|
-
},
|
|
3925
|
-
};
|
|
3926
|
-
});
|
|
3927
|
-
}
|
|
3928
|
-
if (parsed.type === 'error') {
|
|
3929
|
-
throw new Error(parsed.error || 'Inference error');
|
|
3930
|
-
}
|
|
3931
|
-
} catch (e) {
|
|
3932
|
-
if (e.message && e.message !== 'Inference error' && !e.message.startsWith('HTTP ')) continue;
|
|
3933
|
-
throw e;
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
|
|
3938
|
-
// Compute final metrics from client-side timing
|
|
3939
|
-
const elapsed = performance.now() - startTime;
|
|
3940
|
-
const ttft = firstTokenTime ? firstTokenTime - startTime : null;
|
|
3941
|
-
const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
|
|
3942
|
-
if (tokenCount > 0) {
|
|
3943
|
-
set((s) => {
|
|
3944
|
-
const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
|
|
3945
|
-
const sessions = s.labSessions.map((sess) => {
|
|
3946
|
-
if (sess.id !== sessionId) return sess;
|
|
3947
|
-
const msgs = [...sess.messages];
|
|
3948
|
-
const last = msgs[msgs.length - 1];
|
|
3949
|
-
if (!last?.metrics) {
|
|
3950
|
-
msgs[msgs.length - 1] = { ...last, metrics: { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed } };
|
|
3951
|
-
}
|
|
3952
|
-
return { ...sess, messages: msgs };
|
|
3953
|
-
});
|
|
3954
|
-
return {
|
|
3955
|
-
labSessions: sessions,
|
|
3956
|
-
labMetrics: { ...s.labMetrics, ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist, totalTokens: s.labMetrics.totalTokens + tokenCount, generationTime: elapsed },
|
|
3957
|
-
};
|
|
3958
|
-
});
|
|
3959
|
-
}
|
|
3960
|
-
} catch (err) {
|
|
3961
|
-
if (err.name === 'AbortError') {
|
|
3962
|
-
// User cancelled — keep whatever content was already streamed
|
|
3963
|
-
} else {
|
|
3964
|
-
set((s) => {
|
|
3965
|
-
const sessions = s.labSessions.map((sess) => {
|
|
3966
|
-
if (sess.id !== sessionId) return sess;
|
|
3967
|
-
const msgs = [...sess.messages];
|
|
3968
|
-
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: `Error: ${err.message}`, error: true };
|
|
3969
|
-
return { ...sess, messages: msgs };
|
|
3970
|
-
});
|
|
3971
|
-
return { labSessions: sessions };
|
|
3972
|
-
});
|
|
3973
|
-
}
|
|
3974
|
-
} finally {
|
|
3975
|
-
set({ labStreaming: false, labAbortController: null });
|
|
3976
|
-
}
|
|
3977
|
-
},
|
|
3978
|
-
|
|
3979
|
-
stopLabInference() {
|
|
3980
|
-
const ctrl = get().labAbortController;
|
|
3981
|
-
if (ctrl) ctrl.abort();
|
|
3982
|
-
},
|
|
3983
|
-
|
|
3984
|
-
saveLabPreset(name) {
|
|
3985
|
-
const st = get();
|
|
3986
|
-
const preset = {
|
|
3987
|
-
id: `preset-${Date.now()}`,
|
|
3988
|
-
name,
|
|
3989
|
-
parameters: { ...st.labParameters },
|
|
3990
|
-
systemPrompt: st.labSystemPrompt,
|
|
3991
|
-
runtimeId: st.labActiveRuntime,
|
|
3992
|
-
model: st.labActiveModel,
|
|
3993
|
-
createdAt: Date.now(),
|
|
3994
|
-
};
|
|
3995
|
-
const presets = [...st.labPresets.filter((p) => p.name !== name), preset];
|
|
3996
|
-
set({ labPresets: presets, labActivePreset: preset.id });
|
|
3997
|
-
persistJSON('groove:labPresets', presets);
|
|
3998
|
-
get().addToast('success', `Preset "${name}" saved`);
|
|
3999
|
-
return preset;
|
|
4000
|
-
},
|
|
4001
|
-
|
|
4002
|
-
loadLabPreset(id) {
|
|
4003
|
-
const preset = get().labPresets.find((p) => p.id === id);
|
|
4004
|
-
if (!preset) return;
|
|
4005
|
-
const updates = {
|
|
4006
|
-
labParameters: { ...preset.parameters },
|
|
4007
|
-
labSystemPrompt: preset.systemPrompt || '',
|
|
4008
|
-
labActivePreset: id,
|
|
4009
|
-
};
|
|
4010
|
-
if (preset.model) updates.labActiveModel = preset.model;
|
|
4011
|
-
set(updates);
|
|
4012
|
-
persistJSON('groove:labParameters', preset.parameters);
|
|
4013
|
-
if (preset.systemPrompt !== undefined) localStorage.setItem('groove:labSystemPrompt', preset.systemPrompt);
|
|
4014
|
-
},
|
|
4015
|
-
|
|
4016
|
-
deleteLabPreset(id) {
|
|
4017
|
-
const presets = get().labPresets.filter((p) => p.id !== id);
|
|
4018
|
-
set({ labPresets: presets, labActivePreset: get().labActivePreset === id ? null : get().labActivePreset });
|
|
4019
|
-
persistJSON('groove:labPresets', presets);
|
|
4020
|
-
get().addToast('success', 'Preset deleted');
|
|
4021
|
-
},
|
|
4022
|
-
|
|
4023
|
-
async launchLabAssistant(backend) {
|
|
4024
|
-
const existing = get().labAssistantAgentId;
|
|
4025
|
-
if (existing) {
|
|
4026
|
-
const agent = get().agents.find((a) => a.id === existing);
|
|
4027
|
-
if (agent && agent.status === 'running') {
|
|
4028
|
-
set({ labAssistantMode: true });
|
|
4029
|
-
return;
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
try {
|
|
4033
|
-
const data = await api.post('/lab/assistant', { backend });
|
|
4034
|
-
localStorage.setItem('groove:labAssistantAgentId', data.agentId);
|
|
4035
|
-
localStorage.setItem('groove:labAssistantBackend', backend);
|
|
4036
|
-
set({ labAssistantAgentId: data.agentId, labAssistantMode: true, labAssistantBackend: backend });
|
|
4037
|
-
get().addToast('info', `Lab Assistant started for ${backend}`);
|
|
4038
|
-
} catch (err) {
|
|
4039
|
-
get().addToast('error', 'Failed to start assistant', err.message);
|
|
4040
|
-
}
|
|
4041
|
-
},
|
|
4042
|
-
|
|
4043
|
-
dismissLabAssistant() {
|
|
4044
|
-
set({ labAssistantMode: false });
|
|
4045
|
-
},
|
|
4046
|
-
|
|
4047
|
-
clearLabAssistant() {
|
|
4048
|
-
const id = get().labAssistantAgentId;
|
|
4049
|
-
if (id) api.delete(`/agents/${encodeURIComponent(id)}`).catch(() => {});
|
|
4050
|
-
localStorage.removeItem('groove:labAssistantAgentId');
|
|
4051
|
-
localStorage.removeItem('groove:labAssistantBackend');
|
|
4052
|
-
set({ labAssistantAgentId: null, labAssistantMode: false, labAssistantBackend: null });
|
|
4053
|
-
},
|
|
4054
|
-
|
|
4055
|
-
setLabAssistantMode(mode) {
|
|
4056
|
-
set({ labAssistantMode: mode });
|
|
4057
|
-
},
|
|
4058
|
-
|
|
4059
|
-
async renameFile(oldPath, newPath) {
|
|
4060
|
-
try {
|
|
4061
|
-
await api.post('/files/rename', { oldPath, newPath });
|
|
4062
|
-
set((s) => {
|
|
4063
|
-
const tabs = s.editorOpenTabs.map((t) => t === oldPath ? newPath : t);
|
|
4064
|
-
const files = { ...s.editorFiles };
|
|
4065
|
-
if (files[oldPath]) { files[newPath] = files[oldPath]; delete files[oldPath]; }
|
|
4066
|
-
const active = s.editorActiveFile === oldPath ? newPath : s.editorActiveFile;
|
|
4067
|
-
return { editorOpenTabs: tabs, editorFiles: files, editorActiveFile: active };
|
|
4068
|
-
});
|
|
4069
|
-
const oldParent = oldPath.includes('/') ? oldPath.split('/').slice(0, -1).join('/') : '';
|
|
4070
|
-
const newParent = newPath.includes('/') ? newPath.split('/').slice(0, -1).join('/') : '';
|
|
4071
|
-
await get().fetchTreeDir(oldParent);
|
|
4072
|
-
if (newParent !== oldParent) await get().fetchTreeDir(newParent);
|
|
4073
|
-
get().addToast('success', 'Renamed');
|
|
4074
|
-
return true;
|
|
4075
|
-
} catch (err) {
|
|
4076
|
-
get().addToast('error', 'Rename failed', err.message);
|
|
4077
|
-
return false;
|
|
4078
|
-
}
|
|
4079
|
-
},
|
|
4080
|
-
|
|
4081
|
-
// ── Integration Agent Install ────────────────────────────
|
|
4082
|
-
|
|
4083
|
-
async installViaExistingAgent(integration, agentId) {
|
|
4084
|
-
const message = buildIntegrationPrompt(integration);
|
|
4085
|
-
await get().instructAgent(agentId, message);
|
|
4086
|
-
get().setActiveView('agents');
|
|
4087
|
-
get().selectAgent(agentId);
|
|
4088
|
-
},
|
|
4089
|
-
|
|
4090
|
-
async spawnIntegrationTeam(integration) {
|
|
4091
|
-
const team = await get().createTeam(integration.name);
|
|
4092
|
-
const prompt = buildIntegrationPrompt(integration);
|
|
4093
|
-
const agent = await get().spawnAgent({ role: 'planner', prompt, teamId: team.id });
|
|
4094
|
-
get().setActiveView('agents');
|
|
4095
|
-
get().selectAgent(agent.id);
|
|
4096
|
-
return agent;
|
|
4097
|
-
},
|
|
4098
1028
|
}));
|
|
4099
|
-
|
|
4100
|
-
function buildIntegrationPrompt(integration) {
|
|
4101
|
-
const lines = [
|
|
4102
|
-
`Set up the "${integration.name}" integration for this project.`,
|
|
4103
|
-
'',
|
|
4104
|
-
];
|
|
4105
|
-
if (integration.description) lines.push(`**Description:** ${integration.description}`);
|
|
4106
|
-
if (integration.npmPackage) lines.push(`**npm package:** ${integration.npmPackage}`);
|
|
4107
|
-
if (integration.authType) lines.push(`**Auth type:** ${integration.authType}`);
|
|
4108
|
-
if (integration.envKeys?.length) {
|
|
4109
|
-
lines.push('', '**Environment keys required:**');
|
|
4110
|
-
for (const k of integration.envKeys) {
|
|
4111
|
-
lines.push(`- \`${k.key}\` — ${k.label}${k.required ? ' (required)' : ''}`);
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
if (integration.setupSteps?.length) {
|
|
4115
|
-
lines.push('', '**Setup steps:**');
|
|
4116
|
-
integration.setupSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
|
|
4117
|
-
}
|
|
4118
|
-
if (integration.setupUrl) lines.push(``, `**Setup URL:** ${integration.setupUrl}`);
|
|
4119
|
-
if (integration.agentInstructions) lines.push('', `**Agent instructions:** ${integration.agentInstructions}`);
|
|
4120
|
-
return lines.join('\n');
|
|
4121
|
-
}
|