@burdenoff/microfe-vibecontrols 2026.525.3 → 2026.525.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agent-manager/ProfilesPanel.js +105 -103
- package/dist/components/agent-manager/ProfilesPanel.js.map +1 -1
- package/dist/components/agents/OpenInBrowserEditorButton.js +24 -14
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/hooks/useActiveAgentProfile.js +33 -14
- package/dist/hooks/useActiveAgentProfile.js.map +1 -1
- package/dist/translations/en.js +1 -1
- package/dist/translations/en.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Card as e } from "../shared/Card.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isUsableProfile as t, useActiveAgentProfile as n } from "../../hooks/useActiveAgentProfile.js";
|
|
3
3
|
import "../shared/index.js";
|
|
4
|
-
import { useCallback as
|
|
5
|
-
import { CheckCircle2 as
|
|
6
|
-
import { gql as
|
|
7
|
-
import { useMutation as
|
|
8
|
-
import { jsx as
|
|
4
|
+
import { useCallback as r, useState as i } from "react";
|
|
5
|
+
import { CheckCircle2 as a, Loader2 as o, Plus as s, Star as c, Trash2 as l, X as u } from "lucide-react";
|
|
6
|
+
import { gql as d } from "@apollo/client";
|
|
7
|
+
import { useMutation as f } from "@apollo/client/react";
|
|
8
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
9
9
|
//#region src/components/agent-manager/ProfilesPanel.tsx
|
|
10
|
-
var
|
|
10
|
+
var h = d`
|
|
11
11
|
mutation CreateVibecontrolsAgentProfile($input: CreateVibecontrolsAgentProfileInput!) {
|
|
12
12
|
createVibecontrolsAgentProfile(input: $input) {
|
|
13
13
|
id
|
|
@@ -16,11 +16,11 @@ var m = u`
|
|
|
16
16
|
status
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
`,
|
|
19
|
+
`, g = d`
|
|
20
20
|
mutation DeleteVibecontrolsAgentProfile($id: ID!) {
|
|
21
21
|
deleteVibecontrolsAgentProfile(id: $id)
|
|
22
22
|
}
|
|
23
|
-
`,
|
|
23
|
+
`, _ = d`
|
|
24
24
|
mutation SetDefaultVibecontrolsAgentProfile($id: ID!) {
|
|
25
25
|
setDefaultVibecontrolsAgentProfile(id: $id) {
|
|
26
26
|
id
|
|
@@ -28,147 +28,147 @@ var m = u`
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
`;
|
|
31
|
-
function
|
|
31
|
+
function v(e) {
|
|
32
32
|
let t = (e ?? "").toLowerCase();
|
|
33
33
|
return t === "running" || t === "active" || t === "healthy" ? "bg-status-success-text" : t === "stopped" || t === "inactive" ? "bg-text-placeholder" : t === "error" || t === "failed" ? "bg-status-error-text" : "bg-status-warning-text";
|
|
34
34
|
}
|
|
35
|
-
function
|
|
36
|
-
let { profiles:
|
|
37
|
-
|
|
38
|
-
let e =
|
|
35
|
+
function y({ agentId: d }) {
|
|
36
|
+
let { profiles: y, active: b, setActive: x, loading: S, refetch: C } = n(d), [w, T] = i(!1), [E, D] = i("default"), [O, k] = i(""), [A, j] = i(null), [M, { loading: N }] = f(h), [P, { loading: F }] = f(g), [I, { loading: L }] = f(_), R = r(async () => {
|
|
37
|
+
j(null);
|
|
38
|
+
let e = E.trim();
|
|
39
39
|
if (!e) {
|
|
40
|
-
|
|
40
|
+
j("Profile name is required");
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
let t =
|
|
44
|
-
if (
|
|
45
|
-
|
|
43
|
+
let t = O.trim() ? Number(O.trim()) : null;
|
|
44
|
+
if (O.trim() && (Number.isNaN(t) || t === null || t <= 0)) {
|
|
45
|
+
j("Port must be a positive number");
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
try {
|
|
49
|
-
await
|
|
50
|
-
agentId:
|
|
49
|
+
await M({ variables: { input: {
|
|
50
|
+
agentId: d,
|
|
51
51
|
name: e,
|
|
52
52
|
port: t
|
|
53
53
|
} } });
|
|
54
54
|
} catch (e) {
|
|
55
|
-
|
|
55
|
+
j(e instanceof Error ? e.message : "Failed to create profile");
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
T(!1), D("default"), k(""), await C();
|
|
59
59
|
}, [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
]),
|
|
66
|
-
if (
|
|
60
|
+
d,
|
|
61
|
+
M,
|
|
62
|
+
E,
|
|
63
|
+
O,
|
|
64
|
+
C
|
|
65
|
+
]), z = r(async (e) => {
|
|
66
|
+
if (j(null), window.confirm(`Delete profile "${e.name}"? This cannot be undone.`)) {
|
|
67
67
|
try {
|
|
68
|
-
await
|
|
68
|
+
await P({ variables: { id: e.id } });
|
|
69
69
|
} catch (e) {
|
|
70
|
-
|
|
70
|
+
j(e instanceof Error ? e.message : "Failed to delete profile");
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
await
|
|
73
|
+
await C();
|
|
74
74
|
}
|
|
75
|
-
}, [
|
|
76
|
-
|
|
75
|
+
}, [P, C]), B = r(async (e) => {
|
|
76
|
+
j(null);
|
|
77
77
|
try {
|
|
78
|
-
await
|
|
78
|
+
await I({ variables: { id: e.id } });
|
|
79
79
|
} catch (e) {
|
|
80
|
-
|
|
80
|
+
j(e instanceof Error ? e.message : "Failed to set default profile");
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
await
|
|
84
|
-
}, [
|
|
85
|
-
return /* @__PURE__ */
|
|
86
|
-
/* @__PURE__ */
|
|
83
|
+
await C();
|
|
84
|
+
}, [I, C]), V = N || F || L;
|
|
85
|
+
return /* @__PURE__ */ m(e, { children: [
|
|
86
|
+
/* @__PURE__ */ m("div", {
|
|
87
87
|
className: "p-4 border-b border-border-default flex items-center justify-between",
|
|
88
|
-
children: [/* @__PURE__ */
|
|
88
|
+
children: [/* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("h3", {
|
|
89
89
|
className: "font-semibold text-text-primary",
|
|
90
90
|
children: "Profiles"
|
|
91
|
-
}), /* @__PURE__ */
|
|
91
|
+
}), /* @__PURE__ */ m("p", {
|
|
92
92
|
className: "text-xs text-text-secondary mt-0.5",
|
|
93
93
|
children: [
|
|
94
94
|
"Each profile runs an isolated CLI on this agent. Active:",
|
|
95
95
|
" ",
|
|
96
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ p("code", {
|
|
97
97
|
className: "font-mono text-text-primary",
|
|
98
|
-
children:
|
|
98
|
+
children: b
|
|
99
99
|
})
|
|
100
100
|
]
|
|
101
|
-
})] }), /* @__PURE__ */
|
|
101
|
+
})] }), /* @__PURE__ */ m("div", {
|
|
102
102
|
className: "flex items-center gap-2",
|
|
103
|
-
children: [/* @__PURE__ */
|
|
103
|
+
children: [/* @__PURE__ */ p("button", {
|
|
104
104
|
type: "button",
|
|
105
|
-
onClick: () => void
|
|
105
|
+
onClick: () => void C(),
|
|
106
106
|
className: "px-2 py-1 text-xs text-text-secondary hover:text-text-primary",
|
|
107
107
|
children: "Refresh"
|
|
108
|
-
}), /* @__PURE__ */
|
|
108
|
+
}), /* @__PURE__ */ m("button", {
|
|
109
109
|
type: "button",
|
|
110
|
-
onClick: () =>
|
|
110
|
+
onClick: () => T((e) => !e),
|
|
111
111
|
className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90",
|
|
112
|
-
children: [/* @__PURE__ */
|
|
112
|
+
children: [/* @__PURE__ */ p(s, { className: "w-4 h-4" }), "New profile"]
|
|
113
113
|
})]
|
|
114
114
|
})]
|
|
115
115
|
}),
|
|
116
|
-
|
|
116
|
+
A && /* @__PURE__ */ p("div", {
|
|
117
117
|
className: "px-4 py-2 bg-status-error-bg/30 border-b border-status-error-text/20 text-sm text-status-error-text",
|
|
118
|
-
children:
|
|
118
|
+
children: A
|
|
119
119
|
}),
|
|
120
|
-
|
|
120
|
+
w && /* @__PURE__ */ m("div", {
|
|
121
121
|
className: "p-4 border-b border-border-default bg-bg-sunken/40 space-y-3",
|
|
122
|
-
children: [/* @__PURE__ */
|
|
122
|
+
children: [/* @__PURE__ */ m("div", {
|
|
123
123
|
className: "grid grid-cols-2 gap-3",
|
|
124
|
-
children: [/* @__PURE__ */
|
|
124
|
+
children: [/* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("label", {
|
|
125
125
|
className: "block text-xs font-medium text-text-secondary mb-1",
|
|
126
126
|
children: "Profile name"
|
|
127
|
-
}), /* @__PURE__ */
|
|
127
|
+
}), /* @__PURE__ */ p("input", {
|
|
128
128
|
type: "text",
|
|
129
|
-
value:
|
|
130
|
-
onChange: (e) =>
|
|
129
|
+
value: E,
|
|
130
|
+
onChange: (e) => D(e.target.value),
|
|
131
131
|
placeholder: "default",
|
|
132
132
|
className: "w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono",
|
|
133
133
|
autoFocus: !0
|
|
134
|
-
})] }), /* @__PURE__ */
|
|
134
|
+
})] }), /* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("label", {
|
|
135
135
|
className: "block text-xs font-medium text-text-secondary mb-1",
|
|
136
136
|
children: "Port (optional)"
|
|
137
|
-
}), /* @__PURE__ */
|
|
137
|
+
}), /* @__PURE__ */ p("input", {
|
|
138
138
|
type: "number",
|
|
139
|
-
value:
|
|
140
|
-
onChange: (e) =>
|
|
139
|
+
value: O,
|
|
140
|
+
onChange: (e) => k(e.target.value),
|
|
141
141
|
placeholder: "auto",
|
|
142
142
|
className: "w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono"
|
|
143
143
|
})] })]
|
|
144
|
-
}), /* @__PURE__ */
|
|
144
|
+
}), /* @__PURE__ */ m("div", {
|
|
145
145
|
className: "flex items-center gap-2",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
146
|
+
children: [/* @__PURE__ */ m("button", {
|
|
147
147
|
type: "button",
|
|
148
|
-
disabled:
|
|
149
|
-
onClick: () => void
|
|
148
|
+
disabled: N,
|
|
149
|
+
onClick: () => void R(),
|
|
150
150
|
className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90 disabled:opacity-50",
|
|
151
|
-
children: [
|
|
152
|
-
}), /* @__PURE__ */
|
|
151
|
+
children: [N ? /* @__PURE__ */ p(o, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ p(a, { className: "w-3.5 h-3.5" }), "Create"]
|
|
152
|
+
}), /* @__PURE__ */ m("button", {
|
|
153
153
|
type: "button",
|
|
154
154
|
onClick: () => {
|
|
155
|
-
|
|
155
|
+
T(!1), j(null);
|
|
156
156
|
},
|
|
157
157
|
className: "inline-flex items-center gap-1 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary",
|
|
158
|
-
children: [/* @__PURE__ */
|
|
158
|
+
children: [/* @__PURE__ */ p(u, { className: "w-3.5 h-3.5" }), "Cancel"]
|
|
159
159
|
})]
|
|
160
160
|
})]
|
|
161
161
|
}),
|
|
162
|
-
|
|
162
|
+
S && y.length === 0 ? /* @__PURE__ */ m("div", {
|
|
163
163
|
className: "p-6 text-center text-text-secondary",
|
|
164
|
-
children: [/* @__PURE__ */
|
|
165
|
-
}) :
|
|
164
|
+
children: [/* @__PURE__ */ p(o, { className: "w-5 h-5 mx-auto mb-2 animate-spin" }), "Loading profiles…"]
|
|
165
|
+
}) : y.length === 0 ? /* @__PURE__ */ m("div", {
|
|
166
166
|
className: "p-6 text-center text-text-secondary",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ p("p", { children: "No profiles configured yet." }), /* @__PURE__ */ m("p", {
|
|
168
168
|
className: "text-xs mt-1",
|
|
169
169
|
children: [
|
|
170
170
|
"The agent will fall back to the implicit ",
|
|
171
|
-
/* @__PURE__ */
|
|
171
|
+
/* @__PURE__ */ p("code", {
|
|
172
172
|
className: "font-mono",
|
|
173
173
|
children: "default"
|
|
174
174
|
}),
|
|
@@ -176,36 +176,36 @@ function v({ agentId: u }) {
|
|
|
176
176
|
"profile until you create one."
|
|
177
177
|
]
|
|
178
178
|
})]
|
|
179
|
-
}) : /* @__PURE__ */
|
|
179
|
+
}) : /* @__PURE__ */ p("div", {
|
|
180
180
|
className: "divide-y divide-border-default",
|
|
181
|
-
children:
|
|
182
|
-
let
|
|
183
|
-
return /* @__PURE__ */
|
|
184
|
-
className: `p-4 flex items-center justify-between gap-4 ${
|
|
185
|
-
children: [/* @__PURE__ */
|
|
181
|
+
children: y.map((e) => {
|
|
182
|
+
let n = e.name === b, r = t(e);
|
|
183
|
+
return /* @__PURE__ */ m("div", {
|
|
184
|
+
className: `p-4 flex items-center justify-between gap-4 ${n ? "bg-action-primary-bg/5" : ""}`,
|
|
185
|
+
children: [/* @__PURE__ */ m("div", {
|
|
186
186
|
className: "flex items-center gap-3 min-w-0",
|
|
187
|
-
children: [/* @__PURE__ */
|
|
188
|
-
className: `h-2 w-2 rounded-full flex-shrink-0 ${
|
|
187
|
+
children: [/* @__PURE__ */ p("span", {
|
|
188
|
+
className: `h-2 w-2 rounded-full flex-shrink-0 ${v(e.status)}`,
|
|
189
189
|
"aria-hidden": !0
|
|
190
|
-
}), /* @__PURE__ */
|
|
190
|
+
}), /* @__PURE__ */ m("div", {
|
|
191
191
|
className: "min-w-0",
|
|
192
|
-
children: [/* @__PURE__ */
|
|
192
|
+
children: [/* @__PURE__ */ m("div", {
|
|
193
193
|
className: "flex items-center gap-2",
|
|
194
194
|
children: [
|
|
195
|
-
/* @__PURE__ */
|
|
195
|
+
/* @__PURE__ */ p("code", {
|
|
196
196
|
className: "font-mono font-medium text-text-primary truncate",
|
|
197
197
|
children: e.name
|
|
198
198
|
}),
|
|
199
|
-
e.isDefault && /* @__PURE__ */
|
|
199
|
+
e.isDefault && /* @__PURE__ */ m("span", {
|
|
200
200
|
className: "inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-action-primary-bg/10 text-action-primary-bg rounded",
|
|
201
|
-
children: [/* @__PURE__ */
|
|
201
|
+
children: [/* @__PURE__ */ p(c, { className: "w-3 h-3" }), "Default"]
|
|
202
202
|
}),
|
|
203
|
-
|
|
203
|
+
n && /* @__PURE__ */ p("span", {
|
|
204
204
|
className: "px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-status-success-bg/40 text-status-success-text rounded",
|
|
205
205
|
children: "Active"
|
|
206
206
|
})
|
|
207
207
|
]
|
|
208
|
-
}), /* @__PURE__ */
|
|
208
|
+
}), /* @__PURE__ */ m("p", {
|
|
209
209
|
className: "text-xs text-text-secondary mt-0.5 truncate",
|
|
210
210
|
children: [
|
|
211
211
|
e.status ?? "unknown",
|
|
@@ -214,29 +214,31 @@ function v({ agentId: u }) {
|
|
|
214
214
|
]
|
|
215
215
|
})]
|
|
216
216
|
})]
|
|
217
|
-
}), /* @__PURE__ */
|
|
217
|
+
}), /* @__PURE__ */ m("div", {
|
|
218
218
|
className: "flex items-center gap-2 flex-shrink-0",
|
|
219
219
|
children: [
|
|
220
|
-
!
|
|
220
|
+
!n && /* @__PURE__ */ p("button", {
|
|
221
221
|
type: "button",
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
disabled: !r,
|
|
223
|
+
title: r ? "Make this the active profile" : "This profile hasn't been started yet (awaiting config), so it can't be the active profile.",
|
|
224
|
+
onClick: () => x(e.name),
|
|
225
|
+
className: "px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded disabled:opacity-50 disabled:cursor-not-allowed",
|
|
224
226
|
children: "Use"
|
|
225
227
|
}),
|
|
226
|
-
!e.isDefault && /* @__PURE__ */
|
|
228
|
+
!e.isDefault && /* @__PURE__ */ m("button", {
|
|
227
229
|
type: "button",
|
|
228
|
-
disabled:
|
|
229
|
-
onClick: () => void
|
|
230
|
+
disabled: V,
|
|
231
|
+
onClick: () => void B(e),
|
|
230
232
|
className: "inline-flex items-center gap-1 px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded disabled:opacity-50",
|
|
231
|
-
children: [/* @__PURE__ */
|
|
233
|
+
children: [/* @__PURE__ */ p(c, { className: "w-3 h-3" }), "Set default"]
|
|
232
234
|
}),
|
|
233
|
-
/* @__PURE__ */
|
|
235
|
+
/* @__PURE__ */ m("button", {
|
|
234
236
|
type: "button",
|
|
235
|
-
disabled:
|
|
237
|
+
disabled: V || e.isDefault,
|
|
236
238
|
title: e.isDefault ? "Cannot delete the default profile" : "Delete",
|
|
237
|
-
onClick: () => void
|
|
239
|
+
onClick: () => void z(e),
|
|
238
240
|
className: "inline-flex items-center gap-1 px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg/30 border border-status-error-text/20 rounded disabled:opacity-50",
|
|
239
|
-
children: [/* @__PURE__ */
|
|
241
|
+
children: [/* @__PURE__ */ p(l, { className: "w-3 h-3" }), "Delete"]
|
|
240
242
|
})
|
|
241
243
|
]
|
|
242
244
|
})]
|
|
@@ -246,6 +248,6 @@ function v({ agentId: u }) {
|
|
|
246
248
|
] });
|
|
247
249
|
}
|
|
248
250
|
//#endregion
|
|
249
|
-
export {
|
|
251
|
+
export { y as ProfilesPanel };
|
|
250
252
|
|
|
251
253
|
//# sourceMappingURL=ProfilesPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfilesPanel.js","names":[],"sources":["../../../src/components/agent-manager/ProfilesPanel.tsx"],"sourcesContent":["/**\n * ProfilesPanel — Phase 5B (multi-profile-per-daemon).\n *\n * Mounted on AgentDetailsPage as the \"Profiles\" tab. Lists every\n * `AgentProfile` row for the current agent and exposes full CRUDL:\n * - List with status indicators\n * - Create (autofilled name \"default\" but editable)\n * - Delete\n * - Set-default\n *\n * Apollo `gql` documents are inlined so the component compiles cleanly\n * before Phase 5A (svc) publishes the AgentProfile schema to Hive.\n */\n\nimport { useCallback, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { CheckCircle2, Loader2, Plus, Star, Trash2, X } from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport { useActiveAgentProfile, type AgentProfile } from '@/hooks/useActiveAgentProfile';\n\ninterface ProfilesPanelProps {\n agentId: string;\n}\n\ninterface CreateAgentProfileInput {\n agentId: string;\n name: string;\n port?: number | null;\n}\n\ninterface CreateProfileData {\n createVibecontrolsAgentProfile: Pick<AgentProfile, 'id' | 'name' | 'isDefault' | 'status'>;\n}\n\ninterface CreateProfileVars {\n input: CreateAgentProfileInput;\n}\n\ninterface DeleteProfileData {\n deleteVibecontrolsAgentProfile: boolean;\n}\n\ninterface DeleteProfileVars {\n id: string;\n}\n\ninterface SetDefaultProfileData {\n setDefaultVibecontrolsAgentProfile: Pick<AgentProfile, 'id' | 'isDefault'>;\n}\n\ninterface SetDefaultProfileVars {\n id: string;\n}\n\nconst CREATE_PROFILE_MUTATION = gql`\n mutation CreateVibecontrolsAgentProfile($input: CreateVibecontrolsAgentProfileInput!) {\n createVibecontrolsAgentProfile(input: $input) {\n id\n name\n isDefault\n status\n }\n }\n`;\n\nconst DELETE_PROFILE_MUTATION = gql`\n mutation DeleteVibecontrolsAgentProfile($id: ID!) {\n deleteVibecontrolsAgentProfile(id: $id)\n }\n`;\n\nconst SET_DEFAULT_PROFILE_MUTATION = gql`\n mutation SetDefaultVibecontrolsAgentProfile($id: ID!) {\n setDefaultVibecontrolsAgentProfile(id: $id) {\n id\n isDefault\n }\n }\n`;\n\nfunction statusDot(status: string | null): string {\n const s = (status ?? '').toLowerCase();\n if (s === 'running' || s === 'active' || s === 'healthy') return 'bg-status-success-text';\n if (s === 'stopped' || s === 'inactive') return 'bg-text-placeholder';\n if (s === 'error' || s === 'failed') return 'bg-status-error-text';\n return 'bg-status-warning-text';\n}\n\nexport function ProfilesPanel({ agentId }: ProfilesPanelProps) {\n const { profiles, active, setActive, loading, refetch } = useActiveAgentProfile(agentId);\n\n const [showCreate, setShowCreate] = useState(false);\n const [newName, setNewName] = useState('default');\n const [newPort, setNewPort] = useState<string>('');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const [createProfile, { loading: creating }] = useMutation<CreateProfileData, CreateProfileVars>(\n CREATE_PROFILE_MUTATION\n );\n\n const [deleteProfile, { loading: deleting }] = useMutation<DeleteProfileData, DeleteProfileVars>(\n DELETE_PROFILE_MUTATION\n );\n\n const [setDefaultProfile, { loading: settingDefault }] = useMutation<\n SetDefaultProfileData,\n SetDefaultProfileVars\n >(SET_DEFAULT_PROFILE_MUTATION);\n\n const handleCreate = useCallback(async () => {\n setActionError(null);\n const trimmedName = newName.trim();\n if (!trimmedName) {\n setActionError('Profile name is required');\n return;\n }\n const portNum = newPort.trim() ? Number(newPort.trim()) : null;\n if (newPort.trim() && (Number.isNaN(portNum) || portNum === null || portNum <= 0)) {\n setActionError('Port must be a positive number');\n return;\n }\n try {\n await createProfile({\n variables: {\n input: { agentId, name: trimmedName, port: portNum },\n },\n });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to create profile');\n return;\n }\n setShowCreate(false);\n setNewName('default');\n setNewPort('');\n // The mutation already succeeded — refresh the list off the same observable\n // the panel renders from so the new profile shows without a manual\n // \"Refresh\". refetch() is non-throwing, so a refresh hiccup can never\n // surface as a \"Failed to create\" error; the list re-syncs on the next action.\n await refetch();\n }, [agentId, createProfile, newName, newPort, refetch]);\n\n const handleDelete = useCallback(\n async (profile: AgentProfile) => {\n setActionError(null);\n if (!window.confirm(`Delete profile \"${profile.name}\"? This cannot be undone.`)) {\n return;\n }\n try {\n await deleteProfile({ variables: { id: profile.id } });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to delete profile');\n return;\n }\n // Refresh after a confirmed delete; refetch() is non-throwing so a\n // network hiccup can't masquerade as a failed delete (see handleCreate).\n await refetch();\n },\n [deleteProfile, refetch]\n );\n\n const handleSetDefault = useCallback(\n async (profile: AgentProfile) => {\n setActionError(null);\n try {\n await setDefaultProfile({ variables: { id: profile.id } });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to set default profile');\n return;\n }\n // Refresh after a confirmed change; refetch() is non-throwing so a\n // network hiccup can't masquerade as a failed update (see handleCreate).\n await refetch();\n },\n [setDefaultProfile, refetch]\n );\n\n const busy = creating || deleting || settingDefault;\n\n return (\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Profiles</h3>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Each profile runs an isolated CLI on this agent. Active:{' '}\n <code className=\"font-mono text-text-primary\">{active}</code>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n // Fire-and-forget from a click handler: refetch() is non-throwing,\n // so ignoring the returned promise can't leak an unhandled rejection.\n onClick={() => void refetch()}\n className=\"px-2 py-1 text-xs text-text-secondary hover:text-text-primary\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => setShowCreate((v) => !v)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90\"\n >\n <Plus className=\"w-4 h-4\" />\n New profile\n </button>\n </div>\n </div>\n\n {actionError && (\n <div className=\"px-4 py-2 bg-status-error-bg/30 border-b border-status-error-text/20 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {showCreate && (\n <div className=\"p-4 border-b border-border-default bg-bg-sunken/40 space-y-3\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n Profile name\n </label>\n <input\n type=\"text\"\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n placeholder=\"default\"\n className=\"w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n autoFocus\n />\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n Port (optional)\n </label>\n <input\n type=\"number\"\n value={newPort}\n onChange={(e) => setNewPort(e.target.value)}\n placeholder=\"auto\"\n className=\"w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={creating}\n onClick={() => void handleCreate()}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90 disabled:opacity-50\"\n >\n {creating ? (\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"w-3.5 h-3.5\" />\n )}\n Create\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n setActionError(null);\n }}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary\"\n >\n <X className=\"w-3.5 h-3.5\" />\n Cancel\n </button>\n </div>\n </div>\n )}\n\n {loading && profiles.length === 0 ? (\n <div className=\"p-6 text-center text-text-secondary\">\n <Loader2 className=\"w-5 h-5 mx-auto mb-2 animate-spin\" />\n Loading profiles…\n </div>\n ) : profiles.length === 0 ? (\n <div className=\"p-6 text-center text-text-secondary\">\n <p>No profiles configured yet.</p>\n <p className=\"text-xs mt-1\">\n The agent will fall back to the implicit <code className=\"font-mono\">default</code>{' '}\n profile until you create one.\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-default\">\n {profiles.map((profile) => {\n const isActive = profile.name === active;\n return (\n <div\n key={profile.id}\n className={`p-4 flex items-center justify-between gap-4 ${\n isActive ? 'bg-action-primary-bg/5' : ''\n }`}\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <span\n className={`h-2 w-2 rounded-full flex-shrink-0 ${statusDot(profile.status)}`}\n aria-hidden\n />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <code className=\"font-mono font-medium text-text-primary truncate\">\n {profile.name}\n </code>\n {profile.isDefault && (\n <span className=\"inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-action-primary-bg/10 text-action-primary-bg rounded\">\n <Star className=\"w-3 h-3\" />\n Default\n </span>\n )}\n {isActive && (\n <span className=\"px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-status-success-bg/40 text-status-success-text rounded\">\n Active\n </span>\n )}\n </div>\n <p className=\"text-xs text-text-secondary mt-0.5 truncate\">\n {profile.status ?? 'unknown'}\n {profile.port ? ` · port ${profile.port}` : ''}\n {profile.tunnelUrl ? ` · ${profile.tunnelUrl}` : ''}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {!isActive && (\n <button\n type=\"button\"\n onClick={() => setActive(profile.name)}\n className=\"px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded\"\n >\n Use\n </button>\n )}\n {!profile.isDefault && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void handleSetDefault(profile)}\n className=\"inline-flex items-center gap-1 px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded disabled:opacity-50\"\n >\n <Star className=\"w-3 h-3\" />\n Set default\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy || profile.isDefault}\n title={profile.isDefault ? 'Cannot delete the default profile' : 'Delete'}\n onClick={() => void handleDelete(profile)}\n className=\"inline-flex items-center gap-1 px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg/30 border border-status-error-text/20 rounded disabled:opacity-50\"\n >\n <Trash2 className=\"w-3 h-3\" />\n Delete\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProfilesPanel.js","names":[],"sources":["../../../src/components/agent-manager/ProfilesPanel.tsx"],"sourcesContent":["/**\n * ProfilesPanel — Phase 5B (multi-profile-per-daemon).\n *\n * Mounted on AgentDetailsPage as the \"Profiles\" tab. Lists every\n * `AgentProfile` row for the current agent and exposes full CRUDL:\n * - List with status indicators\n * - Create (autofilled name \"default\" but editable)\n * - Delete\n * - Set-default\n *\n * Apollo `gql` documents are inlined so the component compiles cleanly\n * before Phase 5A (svc) publishes the AgentProfile schema to Hive.\n */\n\nimport { useCallback, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { CheckCircle2, Loader2, Plus, Star, Trash2, X } from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport {\n useActiveAgentProfile,\n isUsableProfile,\n type AgentProfile,\n} from '@/hooks/useActiveAgentProfile';\n\ninterface ProfilesPanelProps {\n agentId: string;\n}\n\ninterface CreateAgentProfileInput {\n agentId: string;\n name: string;\n port?: number | null;\n}\n\ninterface CreateProfileData {\n createVibecontrolsAgentProfile: Pick<AgentProfile, 'id' | 'name' | 'isDefault' | 'status'>;\n}\n\ninterface CreateProfileVars {\n input: CreateAgentProfileInput;\n}\n\ninterface DeleteProfileData {\n deleteVibecontrolsAgentProfile: boolean;\n}\n\ninterface DeleteProfileVars {\n id: string;\n}\n\ninterface SetDefaultProfileData {\n setDefaultVibecontrolsAgentProfile: Pick<AgentProfile, 'id' | 'isDefault'>;\n}\n\ninterface SetDefaultProfileVars {\n id: string;\n}\n\nconst CREATE_PROFILE_MUTATION = gql`\n mutation CreateVibecontrolsAgentProfile($input: CreateVibecontrolsAgentProfileInput!) {\n createVibecontrolsAgentProfile(input: $input) {\n id\n name\n isDefault\n status\n }\n }\n`;\n\nconst DELETE_PROFILE_MUTATION = gql`\n mutation DeleteVibecontrolsAgentProfile($id: ID!) {\n deleteVibecontrolsAgentProfile(id: $id)\n }\n`;\n\nconst SET_DEFAULT_PROFILE_MUTATION = gql`\n mutation SetDefaultVibecontrolsAgentProfile($id: ID!) {\n setDefaultVibecontrolsAgentProfile(id: $id) {\n id\n isDefault\n }\n }\n`;\n\nfunction statusDot(status: string | null): string {\n const s = (status ?? '').toLowerCase();\n if (s === 'running' || s === 'active' || s === 'healthy') return 'bg-status-success-text';\n if (s === 'stopped' || s === 'inactive') return 'bg-text-placeholder';\n if (s === 'error' || s === 'failed') return 'bg-status-error-text';\n return 'bg-status-warning-text';\n}\n\nexport function ProfilesPanel({ agentId }: ProfilesPanelProps) {\n const { profiles, active, setActive, loading, refetch } = useActiveAgentProfile(agentId);\n\n const [showCreate, setShowCreate] = useState(false);\n const [newName, setNewName] = useState('default');\n const [newPort, setNewPort] = useState<string>('');\n const [actionError, setActionError] = useState<string | null>(null);\n\n const [createProfile, { loading: creating }] = useMutation<CreateProfileData, CreateProfileVars>(\n CREATE_PROFILE_MUTATION\n );\n\n const [deleteProfile, { loading: deleting }] = useMutation<DeleteProfileData, DeleteProfileVars>(\n DELETE_PROFILE_MUTATION\n );\n\n const [setDefaultProfile, { loading: settingDefault }] = useMutation<\n SetDefaultProfileData,\n SetDefaultProfileVars\n >(SET_DEFAULT_PROFILE_MUTATION);\n\n const handleCreate = useCallback(async () => {\n setActionError(null);\n const trimmedName = newName.trim();\n if (!trimmedName) {\n setActionError('Profile name is required');\n return;\n }\n const portNum = newPort.trim() ? Number(newPort.trim()) : null;\n if (newPort.trim() && (Number.isNaN(portNum) || portNum === null || portNum <= 0)) {\n setActionError('Port must be a positive number');\n return;\n }\n try {\n await createProfile({\n variables: {\n input: { agentId, name: trimmedName, port: portNum },\n },\n });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to create profile');\n return;\n }\n setShowCreate(false);\n setNewName('default');\n setNewPort('');\n // The mutation already succeeded — refresh the list off the same observable\n // the panel renders from so the new profile shows without a manual\n // \"Refresh\". refetch() is non-throwing, so a refresh hiccup can never\n // surface as a \"Failed to create\" error; the list re-syncs on the next action.\n await refetch();\n }, [agentId, createProfile, newName, newPort, refetch]);\n\n const handleDelete = useCallback(\n async (profile: AgentProfile) => {\n setActionError(null);\n if (!window.confirm(`Delete profile \"${profile.name}\"? This cannot be undone.`)) {\n return;\n }\n try {\n await deleteProfile({ variables: { id: profile.id } });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to delete profile');\n return;\n }\n // Refresh after a confirmed delete; refetch() is non-throwing so a\n // network hiccup can't masquerade as a failed delete (see handleCreate).\n await refetch();\n },\n [deleteProfile, refetch]\n );\n\n const handleSetDefault = useCallback(\n async (profile: AgentProfile) => {\n setActionError(null);\n try {\n await setDefaultProfile({ variables: { id: profile.id } });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to set default profile');\n return;\n }\n // Refresh after a confirmed change; refetch() is non-throwing so a\n // network hiccup can't masquerade as a failed update (see handleCreate).\n await refetch();\n },\n [setDefaultProfile, refetch]\n );\n\n const busy = creating || deleting || settingDefault;\n\n return (\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Profiles</h3>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Each profile runs an isolated CLI on this agent. Active:{' '}\n <code className=\"font-mono text-text-primary\">{active}</code>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n // Fire-and-forget from a click handler: refetch() is non-throwing,\n // so ignoring the returned promise can't leak an unhandled rejection.\n onClick={() => void refetch()}\n className=\"px-2 py-1 text-xs text-text-secondary hover:text-text-primary\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => setShowCreate((v) => !v)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90\"\n >\n <Plus className=\"w-4 h-4\" />\n New profile\n </button>\n </div>\n </div>\n\n {actionError && (\n <div className=\"px-4 py-2 bg-status-error-bg/30 border-b border-status-error-text/20 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {showCreate && (\n <div className=\"p-4 border-b border-border-default bg-bg-sunken/40 space-y-3\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n Profile name\n </label>\n <input\n type=\"text\"\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n placeholder=\"default\"\n className=\"w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n autoFocus\n />\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n Port (optional)\n </label>\n <input\n type=\"number\"\n value={newPort}\n onChange={(e) => setNewPort(e.target.value)}\n placeholder=\"auto\"\n className=\"w-full px-3 py-1.5 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={creating}\n onClick={() => void handleCreate()}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90 disabled:opacity-50\"\n >\n {creating ? (\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"w-3.5 h-3.5\" />\n )}\n Create\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n setActionError(null);\n }}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary\"\n >\n <X className=\"w-3.5 h-3.5\" />\n Cancel\n </button>\n </div>\n </div>\n )}\n\n {loading && profiles.length === 0 ? (\n <div className=\"p-6 text-center text-text-secondary\">\n <Loader2 className=\"w-5 h-5 mx-auto mb-2 animate-spin\" />\n Loading profiles…\n </div>\n ) : profiles.length === 0 ? (\n <div className=\"p-6 text-center text-text-secondary\">\n <p>No profiles configured yet.</p>\n <p className=\"text-xs mt-1\">\n The agent will fall back to the implicit <code className=\"font-mono\">default</code>{' '}\n profile until you create one.\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-default\">\n {profiles.map((profile) => {\n const isActive = profile.name === active;\n // A never-started (awaiting-config) profile can't be the active\n // one — selecting it would just bounce back to a usable profile.\n // Disable \"Use\" and explain why instead of silently reverting.\n const usable = isUsableProfile(profile);\n return (\n <div\n key={profile.id}\n className={`p-4 flex items-center justify-between gap-4 ${\n isActive ? 'bg-action-primary-bg/5' : ''\n }`}\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <span\n className={`h-2 w-2 rounded-full flex-shrink-0 ${statusDot(profile.status)}`}\n aria-hidden\n />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <code className=\"font-mono font-medium text-text-primary truncate\">\n {profile.name}\n </code>\n {profile.isDefault && (\n <span className=\"inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-action-primary-bg/10 text-action-primary-bg rounded\">\n <Star className=\"w-3 h-3\" />\n Default\n </span>\n )}\n {isActive && (\n <span className=\"px-1.5 py-0.5 text-[10px] uppercase tracking-wide bg-status-success-bg/40 text-status-success-text rounded\">\n Active\n </span>\n )}\n </div>\n <p className=\"text-xs text-text-secondary mt-0.5 truncate\">\n {profile.status ?? 'unknown'}\n {profile.port ? ` · port ${profile.port}` : ''}\n {profile.tunnelUrl ? ` · ${profile.tunnelUrl}` : ''}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {!isActive && (\n <button\n type=\"button\"\n disabled={!usable}\n title={\n usable\n ? 'Make this the active profile'\n : \"This profile hasn't been started yet (awaiting config), so it can't be the active profile.\"\n }\n onClick={() => setActive(profile.name)}\n className=\"px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Use\n </button>\n )}\n {!profile.isDefault && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void handleSetDefault(profile)}\n className=\"inline-flex items-center gap-1 px-2 py-1 text-xs text-text-secondary hover:text-text-primary border border-border-default rounded disabled:opacity-50\"\n >\n <Star className=\"w-3 h-3\" />\n Set default\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy || profile.isDefault}\n title={profile.isDefault ? 'Cannot delete the default profile' : 'Delete'}\n onClick={() => void handleDelete(profile)}\n className=\"inline-flex items-center gap-1 px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg/30 border border-status-error-text/20 rounded disabled:opacity-50\"\n >\n <Trash2 className=\"w-3 h-3\" />\n Delete\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -39,23 +39,29 @@ function m({ tunnelUrl: u, agentApiKey: d, profile: m, workspacePath: h, mode: g
|
|
|
39
39
|
C(null);
|
|
40
40
|
try {
|
|
41
41
|
x("checking");
|
|
42
|
-
let e =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
let e = m === "default" ? ["default"] : [m, "default"], t = null, n = null;
|
|
43
|
+
for (let r of e) {
|
|
44
|
+
let i = r === e[e.length - 1], a = await l(u, f(r, "/status"), d);
|
|
45
|
+
if (a.status !== 404 && !(a.status >= 500 && !i)) {
|
|
46
|
+
if (!a.ok) throw Error(`Status check failed: ${a.status}`);
|
|
47
|
+
t = r, n = await a.json();
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!t || !n) throw Error(y("vibecontrols.agents.editor.notInstalled", "Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor."));
|
|
52
|
+
if (!n.installed && !n.installing) {
|
|
47
53
|
x("installing");
|
|
48
|
-
let e = await l(u, f(
|
|
54
|
+
let e = await l(u, f(t, "/install"), d, { method: "POST" });
|
|
49
55
|
if (!e.ok) throw Error(`Install failed: ${e.status}`);
|
|
50
|
-
await p(u, d,
|
|
51
|
-
} else
|
|
52
|
-
if (
|
|
56
|
+
n = await p(u, d, t, (e) => e.installed, (e) => e.error || null);
|
|
57
|
+
} else n.installing && (x("installing"), n = await p(u, d, t, (e) => e.installed, (e) => e.error || null));
|
|
58
|
+
if (n.running) h && n.running && (x("starting"), await l(u, f(t, "/restart"), d, {
|
|
53
59
|
method: "POST",
|
|
54
60
|
body: JSON.stringify({ workspacePath: h })
|
|
55
61
|
}), await new Promise((e) => setTimeout(e, 1e3)));
|
|
56
62
|
else {
|
|
57
63
|
x("starting");
|
|
58
|
-
let e = await l(u, f(
|
|
64
|
+
let e = await l(u, f(t, "/start"), d, {
|
|
59
65
|
method: "POST",
|
|
60
66
|
...h ? { body: JSON.stringify({ workspacePath: h }) } : {}
|
|
61
67
|
});
|
|
@@ -66,8 +72,8 @@ function m({ tunnelUrl: u, agentApiKey: d, profile: m, workspacePath: h, mode: g
|
|
|
66
72
|
await new Promise((e) => setTimeout(e, 1e3));
|
|
67
73
|
}
|
|
68
74
|
x("ready");
|
|
69
|
-
let
|
|
70
|
-
g === "iframe" && _ ? _(
|
|
75
|
+
let r = `${u.replace(/\/$/, "")}/code-server/?apiKey=${encodeURIComponent(d)}`;
|
|
76
|
+
g === "iframe" && _ ? _(r) : window.open(r, "_blank"), setTimeout(() => x("idle"), 2e3);
|
|
71
77
|
} catch (e) {
|
|
72
78
|
C(e instanceof Error ? e.message : "Failed to open editor"), x("error");
|
|
73
79
|
}
|
|
@@ -110,8 +116,12 @@ function m({ tunnelUrl: u, agentApiKey: d, profile: m, workspacePath: h, mode: g
|
|
|
110
116
|
children: E
|
|
111
117
|
})]
|
|
112
118
|
}), b === "error" && S && /* @__PURE__ */ s("div", {
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
role: "alert",
|
|
120
|
+
className: "absolute top-full right-0 mt-1.5 z-50 w-72 rounded-md border border-status-error-text/30 bg-bg-surface p-3 text-xs leading-relaxed text-status-error-text whitespace-normal break-words shadow-lg",
|
|
121
|
+
children: /* @__PURE__ */ c("div", {
|
|
122
|
+
className: "flex items-start gap-2",
|
|
123
|
+
children: [/* @__PURE__ */ s(r, { className: "mt-0.5 h-4 w-4 shrink-0" }), /* @__PURE__ */ s("span", { children: S })]
|
|
124
|
+
})
|
|
115
125
|
})]
|
|
116
126
|
});
|
|
117
127
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n // Only advertise a JSON body when we actually send one. The agent's\n // `/api/*` delegation consumes the request body once; a POST that carries a\n // Content-Type but an (already-read) body fails on the agent with\n // \"ReadableStream has already been used\". No body → no Content-Type → safe.\n const hasBody = options?.body != null;\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n ...(hasBody ? { 'Content-Type': 'application/json' } : {}),\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Build a profile-scoped agent endpoint path. The agent moved every\n * data-plane endpoint under `/api/profiles/<profile>/…` and now returns\n * 410 Gone for bare `/api/code-server/*` paths — hitting the old paths\n * surfaced in the browser as a misleading CORS error (the 410 response\n * omits Access-Control-Allow-Origin).\n */\nfunction codeServerPath(profile: string, sub: string): string {\n return `/api/profiles/${encodeURIComponent(profile)}/code-server${sub}`;\n}\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, codeServerPath(profile, '/status'), apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status\n setState('checking');\n const res = await agentFetch(tunnelUrl, codeServerPath(profile, '/status'), agentApiKey);\n if (res.status === 404) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't available on this agent. Install the code-server plugin (Plugins tab) to enable Open Editor.\"\n )\n );\n }\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(\n tunnelUrl,\n codeServerPath(profile, '/install'),\n agentApiKey,\n {\n method: 'POST',\n }\n );\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(\n tunnelUrl,\n codeServerPath(profile, '/start'),\n agentApiKey,\n {\n method: 'POST',\n ...(workspacePath ? { body: JSON.stringify({ workspacePath }) } : {}),\n }\n );\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n // workspacePath is guaranteed truthy by the `else if` guard above.\n await agentFetch(tunnelUrl, codeServerPath(profile, '/restart'), agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl, tr]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openEditor', 'Open Editor');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error tooltip */}\n {state === 'error' && errorMsg && (\n <div className=\"absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]\">\n {errorMsg}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n // Only advertise a JSON body when we actually send one. The agent's\n // `/api/*` delegation consumes the request body once; a POST that carries a\n // Content-Type but an (already-read) body fails on the agent with\n // \"ReadableStream has already been used\". No body → no Content-Type → safe.\n const hasBody = options?.body != null;\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n ...(hasBody ? { 'Content-Type': 'application/json' } : {}),\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Build a profile-scoped agent endpoint path. The agent moved every\n * data-plane endpoint under `/api/profiles/<profile>/…` and now returns\n * 410 Gone for bare `/api/code-server/*` paths — hitting the old paths\n * surfaced in the browser as a misleading CORS error (the 410 response\n * omits Access-Control-Allow-Origin).\n */\nfunction codeServerPath(profile: string, sub: string): string {\n return `/api/profiles/${encodeURIComponent(profile)}/code-server${sub}`;\n}\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, codeServerPath(profile, '/status'), apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status.\n //\n // code-server runs once per agent (daemon-wide) but its management API\n // is profile-scoped. The active profile may be a not-yet-started profile\n // that returns 404 — which means \"not on this profile\", NOT \"not\n // installed\". So probe the active profile first, then fall back to the\n // daemon's \"default\" boot profile. A 404 (not on this profile) or a\n // transient 5xx (daemon restarting/unreachable) on a non-final candidate\n // falls through to the next; only when every candidate is exhausted do we\n // conclude code-server is genuinely absent (or surface a real error).\n setState('checking');\n const candidates = profile === 'default' ? ['default'] : [profile, 'default'];\n let effectiveProfile: string | null = null;\n let status: CodeServerStatus | null = null;\n for (const candidate of candidates) {\n const isLastCandidate = candidate === candidates[candidates.length - 1];\n const res = await agentFetch(tunnelUrl, codeServerPath(candidate, '/status'), agentApiKey);\n if (res.status === 404) continue; // not on this profile — try the next\n // Transient daemon error on a non-final candidate: the active profile\n // may be flaky while code-server on `default` could still answer.\n if (res.status >= 500 && !isLastCandidate) continue;\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n effectiveProfile = candidate;\n status = await res.json();\n break;\n }\n if (!effectiveProfile || !status) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor.\"\n )\n );\n }\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(\n tunnelUrl,\n codeServerPath(effectiveProfile, '/install'),\n agentApiKey,\n {\n method: 'POST',\n }\n );\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed, and adopt the polled status so the\n // running check below reflects the post-install state rather than\n // the stale initial probe (avoids a redundant /start if code-server\n // ever auto-starts on install).\n status = await pollUntil(\n tunnelUrl,\n agentApiKey,\n effectiveProfile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n status = await pollUntil(\n tunnelUrl,\n agentApiKey,\n effectiveProfile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(\n tunnelUrl,\n codeServerPath(effectiveProfile, '/start'),\n agentApiKey,\n {\n method: 'POST',\n ...(workspacePath ? { body: JSON.stringify({ workspacePath }) } : {}),\n }\n );\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n // workspacePath is guaranteed truthy by the `else if` guard above.\n await agentFetch(tunnelUrl, codeServerPath(effectiveProfile, '/restart'), agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl, tr]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openEditor', 'Open Editor');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error popover. Anchored to the right edge (the button sits at the\n header's right side) with a fixed width so the message wraps as\n normal prose instead of being squeezed into a narrow column by the\n inline-flex wrapper. */}\n {state === 'error' && errorMsg && (\n <div\n role=\"alert\"\n className=\"absolute top-full right-0 mt-1.5 z-50 w-72 rounded-md border border-status-error-text/30 bg-bg-surface p-3 text-xs leading-relaxed text-status-error-text whitespace-normal break-words shadow-lg\"\n >\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" />\n <span>{errorMsg}</span>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|