groove-dev 0.27.142 → 0.27.144
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
- 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 +889 -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-BcoF6_eF.js +1012 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.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/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/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 +144 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- 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/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/editor/selection-menu.jsx +2 -0
- 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 +195 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
- 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/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -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 +452 -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 +227 -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 +17 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
- 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/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/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 +889 -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-BcoF6_eF.js +1012 -0
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/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 +144 -31
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- 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/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/editor/selection-menu.jsx +2 -0
- 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 +195 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
- 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/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +24 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +34 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +452 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +227 -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 +17 -6
- package/packages/gui/src/views/models.jsx +410 -509
- 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-Bjd91ufV.js +0 -984
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
- 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-Bjd91ufV.js +0 -984
- package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
- 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 });
|
|
@@ -325,15 +120,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
325
120
|
if (arr.length > 200) timeline[agent.id] = arr.slice(-200);
|
|
326
121
|
}
|
|
327
122
|
}
|
|
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
123
|
const st = get();
|
|
334
124
|
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
125
|
const prev = st.agents;
|
|
338
126
|
const changed = msg.data.length !== prev.length || msg.data.some((a, i) => {
|
|
339
127
|
const p = prev[i];
|
|
@@ -342,7 +130,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
342
130
|
});
|
|
343
131
|
set({ agents: changed ? msg.data : prev, tokenTimeline: timeline, hydrated: true });
|
|
344
132
|
|
|
345
|
-
// Poll for recommended-team.json while a planner is running
|
|
346
133
|
const hasRunningPlanner = msg.data.some((a) => a.role === 'planner' && a.status === 'running');
|
|
347
134
|
if (hasRunningPlanner && !plannerPollInterval && !get().recommendedTeam) {
|
|
348
135
|
plannerPollInterval = setInterval(() => {
|
|
@@ -376,7 +163,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
376
163
|
if (upd) { found++; return upd; }
|
|
377
164
|
return a;
|
|
378
165
|
});
|
|
379
|
-
// New agents not yet in the list
|
|
380
166
|
if (found < changed.length) {
|
|
381
167
|
for (const a of changed) {
|
|
382
168
|
if (!agents.some((ex) => ex.id === a.id)) agents.push(a);
|
|
@@ -402,7 +188,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
402
188
|
case 'agent:output': {
|
|
403
189
|
const { agentId, data } = msg;
|
|
404
190
|
|
|
405
|
-
// Separate text content from tool calls
|
|
406
191
|
let chatText = '';
|
|
407
192
|
let activityText = '';
|
|
408
193
|
if (typeof data.data === 'string') {
|
|
@@ -415,7 +200,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
415
200
|
}).join('\n');
|
|
416
201
|
}
|
|
417
202
|
|
|
418
|
-
// Update agent metrics in real-time (contextUsage, tokensUsed)
|
|
419
203
|
if (data.contextUsage !== undefined || data.tokensUsed !== undefined) {
|
|
420
204
|
const agents = get().agents.map((a) => {
|
|
421
205
|
if (a.id !== agentId) return a;
|
|
@@ -427,17 +211,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
427
211
|
set({ agents });
|
|
428
212
|
}
|
|
429
213
|
|
|
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
214
|
const isTokenStream = data.subtype === 'stream';
|
|
435
215
|
const showAsChat = chatText && chatText.trim() && !isTokenStream && (
|
|
436
216
|
data.subtype === 'assistant' || data.subtype === 'text' || data.type === 'result' ||
|
|
437
217
|
(data.type === 'activity' && typeof data.data === 'string')
|
|
438
218
|
);
|
|
439
219
|
if (showAsChat) {
|
|
440
|
-
// Clear thinking indicator only when actual text renders as a chat bubble
|
|
441
220
|
if (get().thinkingAgents.has(agentId)) {
|
|
442
221
|
set((s) => {
|
|
443
222
|
const next = new Set(s.thinkingAgents);
|
|
@@ -453,17 +232,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
453
232
|
const last = arr[arr.length - 1];
|
|
454
233
|
const isRecent = last && last.from === 'agent' && (Date.now() - last.timestamp) < 8000;
|
|
455
234
|
|
|
456
|
-
// Skip duplicate text — Claude Code sends 'assistant' then 'result' with same content
|
|
457
235
|
const isDupe = isRecent && (last.text === trimmed || last.text.endsWith(trimmed));
|
|
458
236
|
|
|
459
237
|
if (!isDupe) {
|
|
460
238
|
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
239
|
const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
|
|
464
240
|
arr[arr.length - 1] = { ...last, text: last.text + sep + trimmed, timestamp: Date.now() };
|
|
465
241
|
} else {
|
|
466
|
-
// New message bubble
|
|
467
242
|
arr.push({ from: 'agent', text: trimmed, timestamp: Date.now() });
|
|
468
243
|
}
|
|
469
244
|
|
|
@@ -472,7 +247,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
472
247
|
persistJSON('groove:chatHistory', history);
|
|
473
248
|
}
|
|
474
249
|
|
|
475
|
-
// Mirror to conversation messages if this agent belongs to a conversation
|
|
476
250
|
const conv = get().conversations.find((c) => c.agentId === agentId);
|
|
477
251
|
if (conv) {
|
|
478
252
|
const convMsgs = { ...get().conversationMessages };
|
|
@@ -495,7 +269,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
495
269
|
}
|
|
496
270
|
}
|
|
497
271
|
|
|
498
|
-
// Tool calls → activity log (shown in streaming bar, not as chat bubbles)
|
|
499
272
|
if (activityText && activityText.trim()) {
|
|
500
273
|
const log = { ...get().activityLog };
|
|
501
274
|
if (!log[agentId]) log[agentId] = [];
|
|
@@ -509,7 +282,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
509
282
|
persistJSON('groove:activityLog', log);
|
|
510
283
|
}
|
|
511
284
|
|
|
512
|
-
// Open-on-write: auto-open files the agent writes in workspace mode
|
|
513
285
|
if (get().workspaceMode && Array.isArray(data.data)) {
|
|
514
286
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'write_file', 'edit_file', 'create_file']);
|
|
515
287
|
for (const block of data.data) {
|
|
@@ -534,7 +306,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
534
306
|
const type = msg.status === 'completed' ? 'success' : isKill ? 'info' : 'warning';
|
|
535
307
|
get().addToast(type, text, msg.error ? msg.error.slice(0, 200) : undefined);
|
|
536
308
|
|
|
537
|
-
// Clear thinking indicator — agent is no longer active
|
|
538
309
|
if (get().thinkingAgents.has(msg.agentId)) {
|
|
539
310
|
set((s) => {
|
|
540
311
|
const next = new Set(s.thinkingAgents);
|
|
@@ -543,17 +314,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
543
314
|
});
|
|
544
315
|
}
|
|
545
316
|
|
|
546
|
-
// Clear conversation streaming state
|
|
547
317
|
const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
|
|
548
318
|
if (exitConv && get().streamingConversationId === exitConv.id) {
|
|
549
319
|
set({ sendingMessage: false, streamingConversationId: null });
|
|
550
320
|
}
|
|
551
321
|
|
|
552
|
-
// Log crash error to agent chat so user can see what happened
|
|
553
322
|
if (msg.error && msg.agentId) {
|
|
554
323
|
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
555
324
|
}
|
|
556
|
-
// Clear workspace if the exiting agent was the workspace target
|
|
557
325
|
if (get().workspaceAgentId === msg.agentId) {
|
|
558
326
|
const teamAgents = get().agents.filter(
|
|
559
327
|
(a) => a.id !== msg.agentId && a.teamId === get().activeTeamId,
|
|
@@ -562,7 +330,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
562
330
|
set({ workspaceAgentId: next?.id || null });
|
|
563
331
|
}
|
|
564
332
|
|
|
565
|
-
// Check for recommended team when planner completes
|
|
566
333
|
if (agent?.role === 'planner' && msg.status === 'completed') {
|
|
567
334
|
setTimeout(() => get().checkRecommendedTeam(), 1000);
|
|
568
335
|
}
|
|
@@ -679,9 +446,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
679
446
|
break;
|
|
680
447
|
|
|
681
448
|
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
449
|
const newId = msg.agentId;
|
|
686
450
|
const oldId = msg.oldAgentId;
|
|
687
451
|
if (!newId || !oldId) break;
|
|
@@ -728,7 +492,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
728
492
|
case 'file:changed': {
|
|
729
493
|
const savedAt = get().editorRecentSaves[msg.path];
|
|
730
494
|
if (savedAt && Date.now() - savedAt < 2000) break;
|
|
731
|
-
// Auto-capture workspace snapshot for diff viewer
|
|
732
495
|
if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
|
|
733
496
|
const existing = get().editorFiles[msg.path];
|
|
734
497
|
if (existing?.content) {
|
|
@@ -779,6 +542,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
779
542
|
|
|
780
543
|
case 'schedule:execute':
|
|
781
544
|
get().addToast('info', `Scheduled agent spawned: ${msg.name || msg.role || 'agent'}`);
|
|
545
|
+
get().fetchAutomations();
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case 'schedule:created':
|
|
549
|
+
case 'schedule:updated':
|
|
550
|
+
case 'schedule:deleted':
|
|
551
|
+
get().fetchAutomations();
|
|
782
552
|
break;
|
|
783
553
|
|
|
784
554
|
case 'gateway:status':
|
|
@@ -957,7 +727,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
957
727
|
}
|
|
958
728
|
set(nsUpdate);
|
|
959
729
|
|
|
960
|
-
// Push snapshot for activity chart
|
|
961
730
|
const wsNodes = nsData.nodes || [];
|
|
962
731
|
const wsOwnId = get().networkNode.nodeId;
|
|
963
732
|
const wsOwn = wsOwnId ? wsNodes.find((n) => (n.node_id || n.nodeId) === wsOwnId) : null;
|
|
@@ -1239,2883 +1008,4 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1239
1008
|
};
|
|
1240
1009
|
ws.onerror = () => ws.close();
|
|
1241
1010
|
},
|
|
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
1011
|
}));
|
|
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
|
-
}
|