@burdenoff/microfe-vibecontrols 2026.709.2 → 2026.710.1

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.
Files changed (45) hide show
  1. package/dist/components/agent-manager/AgentManagerBookmarksDrawer.js +87 -85
  2. package/dist/components/agent-manager/AgentManagerBookmarksDrawer.js.map +1 -1
  3. package/dist/components/ai/InstructionFilesManager.js +138 -136
  4. package/dist/components/ai/InstructionFilesManager.js.map +1 -1
  5. package/dist/components/ai/PluginHarnessPicker.js +70 -64
  6. package/dist/components/ai/PluginHarnessPicker.js.map +1 -1
  7. package/dist/components/docs/DocsEditPanel.js +1 -1
  8. package/dist/components/docs/DocsEditPanel.js.map +1 -1
  9. package/dist/components/docs/DocsPageTree.js +1 -1
  10. package/dist/components/docs/DocsPageTree.js.map +1 -1
  11. package/dist/components/docs/DocsTabPanel.js +108 -106
  12. package/dist/components/docs/DocsTabPanel.js.map +1 -1
  13. package/dist/components/notes/NotesTabPanel.js +99 -99
  14. package/dist/components/notes/NotesTabPanel.js.map +1 -1
  15. package/dist/components/scorecard/ScorecardPanel.js +309 -308
  16. package/dist/components/scorecard/ScorecardPanel.js.map +1 -1
  17. package/dist/components/sessions/SessionPublicLinkPanel.js +104 -102
  18. package/dist/components/sessions/SessionPublicLinkPanel.js.map +1 -1
  19. package/dist/components/settings/AdvancedSection.js +92 -90
  20. package/dist/components/settings/AdvancedSection.js.map +1 -1
  21. package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js +104 -102
  22. package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js.map +1 -1
  23. package/dist/constants/permissions.js +7 -0
  24. package/dist/constants/permissions.js.map +1 -1
  25. package/dist/pages/ActionsPage.js +303 -303
  26. package/dist/pages/ActionsPage.js.map +1 -1
  27. package/dist/pages/AgentDetailsPage.js +520 -516
  28. package/dist/pages/AgentDetailsPage.js.map +1 -1
  29. package/dist/pages/AgentsPage.js +287 -283
  30. package/dist/pages/AgentsPage.js.map +1 -1
  31. package/dist/pages/TargetsPage.js +554 -549
  32. package/dist/pages/TargetsPage.js.map +1 -1
  33. package/dist/pages/VibeAuditPage.js +2 -2
  34. package/dist/pages/VibeAuditPage.js.map +1 -1
  35. package/dist/pages/VibeDeckPage.js +257 -257
  36. package/dist/pages/VibeDeckPage.js.map +1 -1
  37. package/dist/pages/VibeDetailsPage.js +688 -693
  38. package/dist/pages/VibeDetailsPage.js.map +1 -1
  39. package/dist/pages/VibesPage.js +49 -41
  40. package/dist/pages/VibesPage.js.map +1 -1
  41. package/dist/pages/WebhooksPage.js +162 -138
  42. package/dist/pages/WebhooksPage.js.map +1 -1
  43. package/dist/pages/tunnels/DomainsTab.js +54 -47
  44. package/dist/pages/tunnels/DomainsTab.js.map +1 -1
  45. package/package.json +1 -1
@@ -2,117 +2,119 @@ import { useRemoveAgentPluginMutation as e } from "../../generated/wspace-operat
2
2
  import { Card as t } from "../shared/Card.js";
3
3
  import { usePluginAvailability as n } from "../../hooks/usePluginAvailability.js";
4
4
  import { getPluginMeta as r } from "../../constants/pluginCatalog.js";
5
- import { useCallback as i, useEffect as a, useRef as o, useState as s } from "react";
6
- import { Check as c, CheckCircle2 as l, Download as u, Loader2 as d, Puzzle as f, Trash2 as p } from "lucide-react";
7
- import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
5
+ import { VIBE_PERMISSIONS as i } from "../../constants/permissions.js";
6
+ import { useCallback as a, useEffect as o, useRef as s, useState as c } from "react";
7
+ import { Check as l, CheckCircle2 as u, Download as d, Loader2 as f, Puzzle as p, Trash2 as m } from "lucide-react";
8
+ import { usePermissions as h } from "@burdenoff/fe-libs/shared/providers/shell";
9
+ import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
8
10
  //#region src/components/ai/PluginHarnessPicker.tsx
9
- function _({ packages: _, agentId: v, title: y, description: b, onChange: x, renderInstalledAction: S }) {
10
- let { installed: C, installing: w, install: T, refetch: E, loading: D, error: O } = n(_, v), [k] = e(), [A, j] = s(null), [M, N] = s({}), [P, F] = s({}), I = o(/* @__PURE__ */ new Map());
11
- a(() => {
12
- let e = I.current;
11
+ function y({ packages: y, agentId: b, title: x, description: S, onChange: C, renderInstalledAction: w }) {
12
+ let { installed: T, installing: E, install: D, refetch: O, loading: k, error: A } = n(y, b), { hasPermission: j } = h(), M = j(i.AGENT_UPDATE), [N] = e(), [P, F] = c(null), [I, L] = c({}), [R, z] = c({}), B = s(/* @__PURE__ */ new Map());
13
+ o(() => {
14
+ let e = B.current;
13
15
  return () => {
14
16
  for (let t of e.values()) clearTimeout(t);
15
17
  e.clear();
16
18
  };
17
19
  }, []);
18
- let L = i(async (e) => {
19
- if (await T(e)) {
20
- F((t) => ({
20
+ let V = a(async (e) => {
21
+ if (await D(e)) {
22
+ z((t) => ({
21
23
  ...t,
22
24
  [e]: !0
23
25
  }));
24
- let t = I.current.get(e);
26
+ let t = B.current.get(e);
25
27
  t && clearTimeout(t);
26
28
  let n = setTimeout(() => {
27
- F((t) => {
29
+ z((t) => {
28
30
  if (!t[e]) return t;
29
31
  let n = { ...t };
30
32
  return delete n[e], n;
31
- }), I.current.delete(e);
33
+ }), B.current.delete(e);
32
34
  }, 1500);
33
- I.current.set(e, n), x?.();
35
+ B.current.set(e, n), C?.();
34
36
  }
35
- }, [T, x]), R = i(async (e) => {
36
- if (v) {
37
- j(e), N((t) => ({
37
+ }, [D, C]), H = a(async (e) => {
38
+ if (b) {
39
+ F(e), L((t) => ({
38
40
  ...t,
39
41
  [e]: ""
40
42
  }));
41
43
  try {
42
- let { data: t } = await k({ variables: {
43
- agentId: v,
44
+ let { data: t } = await N({ variables: {
45
+ agentId: b,
44
46
  packageName: e
45
47
  } });
46
48
  if (!t?.removeAgentPlugin.success) {
47
- N((n) => ({
49
+ L((n) => ({
48
50
  ...n,
49
51
  [e]: t?.removeAgentPlugin.error || "Remove failed"
50
52
  }));
51
53
  return;
52
54
  }
53
- await E(), x?.();
55
+ await O(), C?.();
54
56
  } catch (t) {
55
- N((n) => ({
57
+ L((n) => ({
56
58
  ...n,
57
59
  [e]: t instanceof Error ? t.message : String(t)
58
60
  }));
59
61
  } finally {
60
- j(null);
62
+ F(null);
61
63
  }
62
64
  }
63
65
  }, [
64
- v,
65
- k,
66
- E,
67
- x
66
+ b,
67
+ N,
68
+ O,
69
+ C
68
70
  ]);
69
- return _.length === 0 ? null : /* @__PURE__ */ g("section", {
71
+ return y.length === 0 ? null : /* @__PURE__ */ v("section", {
70
72
  className: "flex flex-col gap-3",
71
73
  "data-testid": "plugin-harness-picker",
72
74
  children: [
73
- (y || b) && /* @__PURE__ */ g("div", { children: [y && /* @__PURE__ */ h("h3", {
75
+ (x || S) && /* @__PURE__ */ v("div", { children: [x && /* @__PURE__ */ _("h3", {
74
76
  className: "text-base font-semibold text-text-primary",
75
- children: y
76
- }), b && /* @__PURE__ */ h("p", {
77
+ children: x
78
+ }), S && /* @__PURE__ */ _("p", {
77
79
  className: "text-sm text-text-secondary mt-1",
78
- children: b
80
+ children: S
79
81
  })] }),
80
- O && /* @__PURE__ */ h("div", {
82
+ A && /* @__PURE__ */ _("div", {
81
83
  role: "alert",
82
84
  className: "rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-xs text-status-error-text",
83
- children: O
85
+ children: A
84
86
  }),
85
- /* @__PURE__ */ h("div", {
87
+ /* @__PURE__ */ _("div", {
86
88
  className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3",
87
- children: _.map((e) => {
88
- let n = r(e), i = !!C[e], a = w === e, o = A === e, s = a || o || D, _ = !!P[e], y = M[e];
89
- return /* @__PURE__ */ g(t, {
89
+ children: y.map((e) => {
90
+ let n = r(e), i = !!T[e], a = E === e, o = P === e, s = a || o || k, c = !!R[e], h = I[e];
91
+ return /* @__PURE__ */ v(t, {
90
92
  padding: "md",
91
93
  className: "flex flex-col gap-3",
92
94
  children: [
93
- /* @__PURE__ */ g("div", {
95
+ /* @__PURE__ */ v("div", {
94
96
  className: "flex items-start gap-3",
95
- children: [/* @__PURE__ */ h("div", {
97
+ children: [/* @__PURE__ */ _("div", {
96
98
  className: "flex-shrink-0 size-9 rounded-md bg-bg-sunken flex items-center justify-center",
97
- children: /* @__PURE__ */ h(f, { className: "size-4 text-text-secondary" })
98
- }), /* @__PURE__ */ g("div", {
99
+ children: /* @__PURE__ */ _(p, { className: "size-4 text-text-secondary" })
100
+ }), /* @__PURE__ */ v("div", {
99
101
  className: "min-w-0 flex-1",
100
102
  children: [
101
- /* @__PURE__ */ g("div", {
103
+ /* @__PURE__ */ v("div", {
102
104
  className: "flex items-start gap-2 flex-wrap",
103
- children: [/* @__PURE__ */ h("h4", {
105
+ children: [/* @__PURE__ */ _("h4", {
104
106
  className: "font-medium text-text-primary break-words max-w-[100ch]",
105
107
  children: n.displayName
106
- }), i && /* @__PURE__ */ g("span", {
108
+ }), i && /* @__PURE__ */ v("span", {
107
109
  className: "inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg-subtle text-status-success-text shrink-0",
108
- children: [/* @__PURE__ */ h(c, { className: "size-3" }), " installed"]
110
+ children: [/* @__PURE__ */ _(l, { className: "size-3" }), " installed"]
109
111
  })]
110
112
  }),
111
- /* @__PURE__ */ h("p", {
113
+ /* @__PURE__ */ _("p", {
112
114
  className: "text-sm text-text-secondary mt-0.5",
113
115
  children: n.description
114
116
  }),
115
- /* @__PURE__ */ h("p", {
117
+ /* @__PURE__ */ _("p", {
116
118
  className: "text-[10px] font-mono text-text-placeholder mt-1 break-all",
117
119
  title: e,
118
120
  children: e
@@ -120,45 +122,49 @@ function _({ packages: _, agentId: v, title: y, description: b, onChange: x, ren
120
122
  ]
121
123
  })]
122
124
  }),
123
- /* @__PURE__ */ g("div", {
125
+ /* @__PURE__ */ v("div", {
124
126
  className: "flex items-center gap-2",
125
127
  children: [
126
- i && _ ? /* @__PURE__ */ g("button", {
128
+ i && c ? /* @__PURE__ */ v("button", {
127
129
  type: "button",
128
130
  disabled: !0,
129
131
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg-subtle text-status-success-text border border-status-success-border cursor-default",
130
132
  "data-testid": `plugin-installed-${e}`,
131
133
  "aria-live": "polite",
132
- children: [/* @__PURE__ */ h(l, { className: "size-3.5" }), "Installed"]
133
- }) : i ? /* @__PURE__ */ g("button", {
134
+ children: [/* @__PURE__ */ _(u, { className: "size-3.5" }), "Installed"]
135
+ }) : i && M ? /* @__PURE__ */ v("button", {
134
136
  type: "button",
135
- disabled: s || !v,
136
- onClick: () => R(e),
137
+ disabled: s || !b,
138
+ onClick: () => H(e),
137
139
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed",
138
140
  "data-testid": `plugin-remove-${e}`,
139
- children: [o ? /* @__PURE__ */ h(d, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ h(p, { className: "size-3.5" }), "Remove"]
141
+ children: [o ? /* @__PURE__ */ _(f, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ _(m, { className: "size-3.5" }), "Remove"]
142
+ }) : i ? /* @__PURE__ */ v("span", {
143
+ className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg-subtle text-status-success-text border border-status-success-border",
144
+ "data-testid": `plugin-installed-static-${e}`,
145
+ children: [/* @__PURE__ */ _(u, { className: "size-3.5" }), "Installed"]
140
146
  }) : null,
141
- i && !_ && S?.(e),
142
- !i && /* @__PURE__ */ h("button", {
147
+ i && !c && w?.(e),
148
+ !i && /* @__PURE__ */ _("button", {
143
149
  type: "button",
144
- disabled: s || !v,
145
- onClick: () => L(e),
150
+ disabled: s || !b,
151
+ onClick: () => V(e),
146
152
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed",
147
153
  "data-testid": `plugin-install-${e}`,
148
154
  "aria-busy": a,
149
- children: a ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(d, { className: "size-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "size-3.5" }), "Install"] })
155
+ children: a ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(f, { className: "size-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(d, { className: "size-3.5" }), "Install"] })
150
156
  })
151
157
  ]
152
158
  }),
153
- y && /* @__PURE__ */ h("p", {
159
+ h && /* @__PURE__ */ _("p", {
154
160
  className: "text-xs text-status-error-text",
155
- children: y
161
+ children: h
156
162
  })
157
163
  ]
158
164
  }, e);
159
165
  })
160
166
  }),
161
- !v && /* @__PURE__ */ h("p", {
167
+ !b && /* @__PURE__ */ _("p", {
162
168
  className: "text-xs text-text-muted",
163
169
  children: "Select an agent to manage plugins."
164
170
  })
@@ -166,6 +172,6 @@ function _({ packages: _, agentId: v, title: y, description: b, onChange: x, ren
166
172
  });
167
173
  }
168
174
  //#endregion
169
- export { _ as PluginHarnessPicker };
175
+ export { y as PluginHarnessPicker };
170
176
 
171
177
  //# sourceMappingURL=PluginHarnessPicker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluginHarnessPicker.js","names":[],"sources":["../../../src/components/ai/PluginHarnessPicker.tsx"],"sourcesContent":["/**\n * <PluginHarnessPicker />\n *\n * Grid of installable VibeControls agent plugins with per-card\n * Install / Installed / Remove state. Driven by the existing\n * `usePluginAvailability` hook (same code path as `<PluginGate>`), so\n * the Plugins tab on the Agent details page and this picker stay in\n * sync automatically.\n *\n * The default use-case is the AI workbench: once the base\n * `@vibecontrols/vibe-plugin-ai` plugin is installed, this picker lists\n * every AI provider harness (claude, codex, copilot, gemini, …) so the\n * user can add / remove harnesses without leaving the AI page.\n *\n * The component is feature-neutral — pass any `packages` array (e.g.\n * session providers, tunnel providers) to reuse the same UI.\n */\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Check, CheckCircle2, Download, Loader2, Puzzle, Trash2 } from 'lucide-react';\nimport { useRemoveAgentPluginMutation } from '@/generated/wspace-operations';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { Card } from '../shared/Card';\n\nexport interface PluginHarnessPickerProps {\n /** Package names to offer. Metadata is resolved via `getPluginMeta`. */\n packages: readonly string[];\n /** Active agent id. When null, the picker renders in a disabled state. */\n agentId: string | null;\n /** Optional section heading (rendered above the grid). */\n title?: string;\n /** Optional section description. */\n description?: string;\n /** Fired after a successful install or remove so parents can refetch. */\n onChange?: () => void;\n /**\n * Optional per-card action slot, rendered alongside the Remove button when\n * a package is installed. Lets a feature (e.g. the Security tab) attach a\n * contextual action — like \"Run scan\" — without coupling this generic\n * picker to that feature. Receives the installed package name.\n */\n renderInstalledAction?: (pkg: string) => ReactNode;\n}\n\nexport function PluginHarnessPicker({\n packages,\n agentId,\n title,\n description,\n onChange,\n renderInstalledAction,\n}: PluginHarnessPickerProps) {\n // Plugins are agent-wide (BOFF-2612); no profile threading needed.\n const { installed, installing, install, refetch, loading, error } = usePluginAvailability(\n packages,\n agentId\n );\n const [removePlugin] = useRemoveAgentPluginMutation();\n const [removingPkg, setRemovingPkg] = useState<string | null>(null);\n const [removeError, setRemoveError] = useState<Record<string, string>>({});\n // Per-package \"just installed\" pulse — flips on for ~1.5s after a\n // successful install so the button shows a clear success affordance\n // (checkmark + \"Installed\") before the card transitions to its\n // installed/Remove state. Without this the button silently flips from\n // spinner to Remove and users miss the success signal.\n const [justInstalled, setJustInstalled] = useState<Record<string, boolean>>({});\n const justInstalledTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(() => {\n const timers = justInstalledTimers.current;\n return () => {\n for (const t of timers.values()) clearTimeout(t);\n timers.clear();\n };\n }, []);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) {\n setJustInstalled((prev) => ({ ...prev, [pkg]: true }));\n const existing = justInstalledTimers.current.get(pkg);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n setJustInstalled((prev) => {\n if (!prev[pkg]) return prev;\n const next = { ...prev };\n delete next[pkg];\n return next;\n });\n justInstalledTimers.current.delete(pkg);\n }, 1500);\n justInstalledTimers.current.set(pkg, timer);\n onChange?.();\n }\n },\n [install, onChange]\n );\n\n const handleRemove = useCallback(\n async (pkg: string) => {\n if (!agentId) return;\n setRemovingPkg(pkg);\n setRemoveError((e) => ({ ...e, [pkg]: '' }));\n try {\n const { data } = await removePlugin({\n variables: { agentId, packageName: pkg },\n });\n if (!data?.removeAgentPlugin.success) {\n setRemoveError((e) => ({\n ...e,\n [pkg]: data?.removeAgentPlugin.error || 'Remove failed',\n }));\n return;\n }\n await refetch();\n onChange?.();\n } catch (e) {\n setRemoveError((err) => ({\n ...err,\n [pkg]: e instanceof Error ? e.message : String(e),\n }));\n } finally {\n setRemovingPkg(null);\n }\n },\n [agentId, removePlugin, refetch, onChange]\n );\n\n if (packages.length === 0) return null;\n\n return (\n <section className=\"flex flex-col gap-3\" data-testid=\"plugin-harness-picker\">\n {(title || description) && (\n <div>\n {title && <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>}\n {description && <p className=\"text-sm text-text-secondary mt-1\">{description}</p>}\n </div>\n )}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-xs text-status-error-text\"\n >\n {error}\n </div>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {packages.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalled = !!installed[pkg];\n const isInstalling = installing === pkg;\n const isRemoving = removingPkg === pkg;\n const isBusy = isInstalling || isRemoving || loading;\n const showJustInstalled = !!justInstalled[pkg];\n const err = removeError[pkg];\n return (\n <Card key={pkg} padding=\"md\" className=\"flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0 size-9 rounded-md bg-bg-sunken flex items-center justify-center\">\n <Puzzle className=\"size-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start gap-2 flex-wrap\">\n {/*\n Plugin display name must wrap freely up to a\n reasonable hard cap so the column never blows up\n the modal layout. break-words allows wrapping on\n arbitrary characters when a name has no spaces;\n max-w bound corresponds to ~100 char-cells (a\n generous upper bound on real-world display names).\n */}\n <h4 className=\"font-medium text-text-primary break-words max-w-[100ch]\">\n {meta.displayName}\n </h4>\n {isInstalled && (\n <span className=\"inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg-subtle text-status-success-text shrink-0\">\n <Check className=\"size-3\" /> installed\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder mt-1 break-all\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {isInstalled && showJustInstalled ? (\n <button\n type=\"button\"\n disabled\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg-subtle text-status-success-text border border-status-success-border cursor-default\"\n data-testid={`plugin-installed-${pkg}`}\n aria-live=\"polite\"\n >\n <CheckCircle2 className=\"size-3.5\" />\n Installed\n </button>\n ) : isInstalled ? (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleRemove(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-remove-${pkg}`}\n >\n {isRemoving ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"size-3.5\" />\n )}\n Remove\n </button>\n ) : null}\n {isInstalled && !showJustInstalled && renderInstalledAction?.(pkg)}\n {!isInstalled && (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleInstall(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n aria-busy={isInstalling}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"size-3.5\" />\n Install\n </>\n )}\n </button>\n )}\n </div>\n {err && <p className=\"text-xs text-status-error-text\">{err}</p>}\n </Card>\n );\n })}\n </div>\n {!agentId && <p className=\"text-xs text-text-muted\">Select an agent to manage plugins.</p>}\n </section>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PluginHarnessPicker.js","names":[],"sources":["../../../src/components/ai/PluginHarnessPicker.tsx"],"sourcesContent":["/**\n * <PluginHarnessPicker />\n *\n * Grid of installable VibeControls agent plugins with per-card\n * Install / Installed / Remove state. Driven by the existing\n * `usePluginAvailability` hook (same code path as `<PluginGate>`), so\n * the Plugins tab on the Agent details page and this picker stay in\n * sync automatically.\n *\n * The default use-case is the AI workbench: once the base\n * `@vibecontrols/vibe-plugin-ai` plugin is installed, this picker lists\n * every AI provider harness (claude, codex, copilot, gemini, …) so the\n * user can add / remove harnesses without leaving the AI page.\n *\n * The component is feature-neutral — pass any `packages` array (e.g.\n * session providers, tunnel providers) to reuse the same UI.\n */\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Check, CheckCircle2, Download, Loader2, Puzzle, Trash2 } from 'lucide-react';\nimport { useRemoveAgentPluginMutation } from '@/generated/wspace-operations';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { Card } from '../shared/Card';\n\nexport interface PluginHarnessPickerProps {\n /** Package names to offer. Metadata is resolved via `getPluginMeta`. */\n packages: readonly string[];\n /** Active agent id. When null, the picker renders in a disabled state. */\n agentId: string | null;\n /** Optional section heading (rendered above the grid). */\n title?: string;\n /** Optional section description. */\n description?: string;\n /** Fired after a successful install or remove so parents can refetch. */\n onChange?: () => void;\n /**\n * Optional per-card action slot, rendered alongside the Remove button when\n * a package is installed. Lets a feature (e.g. the Security tab) attach a\n * contextual action — like \"Run scan\" — without coupling this generic\n * picker to that feature. Receives the installed package name.\n */\n renderInstalledAction?: (pkg: string) => ReactNode;\n}\n\nexport function PluginHarnessPicker({\n packages,\n agentId,\n title,\n description,\n onChange,\n renderInstalledAction,\n}: PluginHarnessPickerProps) {\n // Plugins are agent-wide (BOFF-2612); no profile threading needed.\n const { installed, installing, install, refetch, loading, error } = usePluginAvailability(\n packages,\n agentId\n );\n const { hasPermission } = usePermissions();\n // removeAgentPlugin is enforced by @rbac(action:\"update\", resource:\"agent\").\n const canRemovePlugin = hasPermission(VIBE_PERMISSIONS.AGENT_UPDATE);\n const [removePlugin] = useRemoveAgentPluginMutation();\n const [removingPkg, setRemovingPkg] = useState<string | null>(null);\n const [removeError, setRemoveError] = useState<Record<string, string>>({});\n // Per-package \"just installed\" pulse — flips on for ~1.5s after a\n // successful install so the button shows a clear success affordance\n // (checkmark + \"Installed\") before the card transitions to its\n // installed/Remove state. Without this the button silently flips from\n // spinner to Remove and users miss the success signal.\n const [justInstalled, setJustInstalled] = useState<Record<string, boolean>>({});\n const justInstalledTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(() => {\n const timers = justInstalledTimers.current;\n return () => {\n for (const t of timers.values()) clearTimeout(t);\n timers.clear();\n };\n }, []);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) {\n setJustInstalled((prev) => ({ ...prev, [pkg]: true }));\n const existing = justInstalledTimers.current.get(pkg);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n setJustInstalled((prev) => {\n if (!prev[pkg]) return prev;\n const next = { ...prev };\n delete next[pkg];\n return next;\n });\n justInstalledTimers.current.delete(pkg);\n }, 1500);\n justInstalledTimers.current.set(pkg, timer);\n onChange?.();\n }\n },\n [install, onChange]\n );\n\n const handleRemove = useCallback(\n async (pkg: string) => {\n if (!agentId) return;\n setRemovingPkg(pkg);\n setRemoveError((e) => ({ ...e, [pkg]: '' }));\n try {\n const { data } = await removePlugin({\n variables: { agentId, packageName: pkg },\n });\n if (!data?.removeAgentPlugin.success) {\n setRemoveError((e) => ({\n ...e,\n [pkg]: data?.removeAgentPlugin.error || 'Remove failed',\n }));\n return;\n }\n await refetch();\n onChange?.();\n } catch (e) {\n setRemoveError((err) => ({\n ...err,\n [pkg]: e instanceof Error ? e.message : String(e),\n }));\n } finally {\n setRemovingPkg(null);\n }\n },\n [agentId, removePlugin, refetch, onChange]\n );\n\n if (packages.length === 0) return null;\n\n return (\n <section className=\"flex flex-col gap-3\" data-testid=\"plugin-harness-picker\">\n {(title || description) && (\n <div>\n {title && <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>}\n {description && <p className=\"text-sm text-text-secondary mt-1\">{description}</p>}\n </div>\n )}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-xs text-status-error-text\"\n >\n {error}\n </div>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {packages.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalled = !!installed[pkg];\n const isInstalling = installing === pkg;\n const isRemoving = removingPkg === pkg;\n const isBusy = isInstalling || isRemoving || loading;\n const showJustInstalled = !!justInstalled[pkg];\n const err = removeError[pkg];\n return (\n <Card key={pkg} padding=\"md\" className=\"flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0 size-9 rounded-md bg-bg-sunken flex items-center justify-center\">\n <Puzzle className=\"size-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start gap-2 flex-wrap\">\n {/*\n Plugin display name must wrap freely up to a\n reasonable hard cap so the column never blows up\n the modal layout. break-words allows wrapping on\n arbitrary characters when a name has no spaces;\n max-w bound corresponds to ~100 char-cells (a\n generous upper bound on real-world display names).\n */}\n <h4 className=\"font-medium text-text-primary break-words max-w-[100ch]\">\n {meta.displayName}\n </h4>\n {isInstalled && (\n <span className=\"inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg-subtle text-status-success-text shrink-0\">\n <Check className=\"size-3\" /> installed\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder mt-1 break-all\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {isInstalled && showJustInstalled ? (\n <button\n type=\"button\"\n disabled\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg-subtle text-status-success-text border border-status-success-border cursor-default\"\n data-testid={`plugin-installed-${pkg}`}\n aria-live=\"polite\"\n >\n <CheckCircle2 className=\"size-3.5\" />\n Installed\n </button>\n ) : isInstalled && canRemovePlugin ? (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleRemove(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-remove-${pkg}`}\n >\n {isRemoving ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"size-3.5\" />\n )}\n Remove\n </button>\n ) : isInstalled ? (\n // Installed, but the actor lacks agent:update to remove it.\n // Show a non-interactive \"Installed\" indicator so the row\n // still reflects state instead of rendering blank.\n <span\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg-subtle text-status-success-text border border-status-success-border\"\n data-testid={`plugin-installed-static-${pkg}`}\n >\n <CheckCircle2 className=\"size-3.5\" />\n Installed\n </span>\n ) : null}\n {isInstalled && !showJustInstalled && renderInstalledAction?.(pkg)}\n {!isInstalled && (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleInstall(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n aria-busy={isInstalling}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"size-3.5\" />\n Install\n </>\n )}\n </button>\n )}\n </div>\n {err && <p className=\"text-xs text-status-error-text\">{err}</p>}\n </Card>\n );\n })}\n </div>\n {!agentId && <p className=\"text-xs text-text-muted\">Select an agent to manage plugins.</p>}\n </section>\n );\n}\n"],"mappings":"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"}
@@ -76,7 +76,7 @@ function h({ site: h, pages: g, onBack: _, onSavePage: v, onAddPage: y, pageAddi
76
76
  title: "Settings",
77
77
  children: /* @__PURE__ */ p(d, { className: "size-4" })
78
78
  }),
79
- /* @__PURE__ */ p("button", {
79
+ T && /* @__PURE__ */ p("button", {
80
80
  type: "button",
81
81
  onClick: T,
82
82
  className: "p-1.5 text-text-muted hover:text-status-error-text transition-colors",
@@ -1 +1 @@
1
- {"version":3,"file":"DocsEditPanel.js","names":[],"sources":["../../../src/components/docs/DocsEditPanel.tsx"],"sourcesContent":["import { useEffect, useState, type ReactNode } from 'react';\nimport { ArrowLeft, Settings, RefreshCw, History, Trash2 } from 'lucide-react';\nimport { DocsPageTree } from './DocsPageTree';\nimport { DocsPageEditor } from './DocsPageEditor';\nimport { DocsSettingsPanel } from './DocsSettingsPanel';\nimport { DocsCompileStatus } from './DocsCompileStatus';\nimport { DocsRevisionHistory } from './DocsRevisionHistory';\nimport type { DocsSite, DocsPage, DocsRevision } from './types';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface DocsEditPanelProps {\n site: DocsSite;\n pages: DocsPage[];\n onBack: () => void;\n onSavePage: (\n id: string,\n data: { title?: string; content?: string; slug?: string }\n ) => Promise<void>;\n onAddPage: (parentId?: string, category?: string) => void;\n pageAdding?: boolean;\n onDeletePage: (pageId: string) => void;\n onCompile: () => void;\n onSaveSettings: (data: {\n name?: string;\n visibility?: 'PRIVATE' | 'PUBLIC';\n provider?: 'DOCUSAURUS' | 'MKDOCS' | 'VITEPRESS';\n providerConfig?: Record<string, unknown>;\n }) => Promise<void>;\n onRollback: (revisionId: string) => Promise<void>;\n onDeleteSite: () => Promise<void>;\n rollingBack: string | null;\n compileRevision: DocsRevision | null;\n onPollRevision?: (id: string) => Promise<DocsRevision | null>;\n compiling: boolean;\n /** Generic grant-based Share affordance (owner-keyed; supplied by the host). */\n shareSlot?: ReactNode;\n}\n\nexport function DocsEditPanel({\n site,\n pages,\n onBack,\n onSavePage,\n onAddPage,\n pageAdding,\n onDeletePage,\n onCompile,\n onSaveSettings,\n onRollback,\n onDeleteSite,\n rollingBack,\n compileRevision,\n onPollRevision,\n compiling,\n shareSlot,\n}: DocsEditPanelProps) {\n const tr = useTr();\n const [selectedPageId, setSelectedPageId] = useState<string | null>(pages[0]?.id || null);\n const [showSettings, setShowSettings] = useState(false);\n const [showHistory, setShowHistory] = useState(false);\n\n const selectedPage = pages.find((p) => p.id === selectedPageId) || null;\n\n useEffect(() => {\n if (pages.length === 0) {\n if (selectedPageId) setSelectedPageId(null);\n return;\n }\n if (!selectedPageId || !pages.some((page) => page.id === selectedPageId)) {\n setSelectedPageId(pages[0].id);\n }\n }, [pages, selectedPageId]);\n\n return (\n <div className=\"flex flex-col h-full\" style={{ minHeight: 'calc(100vh - 380px)' }}>\n {/* Top bar */}\n <div className=\"flex items-center justify-between px-4 py-2 border-b border-border-default bg-bg-surface\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={onBack}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibecontrols.docs.edit.preview', 'Preview')}\n </button>\n <span className=\"text-text-muted\">|</span>\n <span className=\"text-sm font-medium text-text-primary\">{site.name}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n {shareSlot}\n <div className=\"relative\">\n <DocsCompileStatus revision={compileRevision} onPoll={onPollRevision} />\n </div>\n <button\n type=\"button\"\n onClick={onCompile}\n disabled={compiling}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n <RefreshCw className={`size-3.5 ${compiling ? 'animate-spin' : ''}`} />\n {tr('vibecontrols.docs.edit.compile', 'Compile')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowHistory((v) => !v);\n setShowSettings(false);\n }}\n className={`p-1.5 transition-colors ${showHistory ? 'text-action-primary-text-alt' : 'text-text-secondary hover:text-text-primary'}`}\n title=\"Build history\"\n >\n <History className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(true);\n setShowHistory(false);\n }}\n className=\"p-1.5 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Settings\"\n >\n <Settings className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={onDeleteSite}\n className=\"p-1.5 text-text-muted hover:text-status-error-text transition-colors\"\n title=\"Delete site\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n </div>\n\n {/* Revision history drawer */}\n {showHistory && (\n <div className=\"border-b border-border-default bg-bg-base max-h-64 overflow-y-auto\">\n <DocsRevisionHistory\n revisions={site.revisions ?? []}\n currentRevisionId={site.currentRevisionId}\n onRollback={onRollback}\n rollingBack={rollingBack}\n />\n </div>\n )}\n\n {/* Main content: sidebar + editor */}\n <div className=\"flex-1 grid grid-cols-[260px_1fr] min-h-0\">\n {/* Page tree sidebar */}\n <div className=\"border-r border-border-default overflow-hidden\">\n <DocsPageTree\n pages={pages}\n selectedPageId={selectedPageId}\n onSelectPage={setSelectedPageId}\n onAddPage={onAddPage}\n pageAdding={pageAdding}\n onDeletePage={onDeletePage}\n />\n </div>\n\n {/* Editor */}\n <div className=\"overflow-hidden\">\n <DocsPageEditor page={selectedPage} onSave={onSavePage} />\n </div>\n </div>\n\n {/* Settings panel */}\n <DocsSettingsPanel\n site={site}\n open={showSettings}\n onClose={() => setShowSettings(false)}\n onSave={onSaveSettings}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAsCA,SAAgB,EAAc,EAC5B,SACA,UACA,WACA,eACA,cACA,eACA,iBACA,cACA,mBACA,eACA,iBACA,gBACA,oBACA,mBACA,cACA,gBACqB;CACrB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAgB,KAAqB,EAAwB,EAAM,IAAI,MAAM,KAAK,EACnF,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAa,KAAkB,EAAS,GAAM,EAE/C,IAAe,EAAM,MAAM,MAAM,EAAE,OAAO,EAAe,IAAI;AAYnE,QAVA,QAAgB;AACd,MAAI,EAAM,WAAW,GAAG;AACtB,GAAI,KAAgB,EAAkB,KAAK;AAC3C;;AAEF,GAAI,CAAC,KAAkB,CAAC,EAAM,MAAM,MAAS,EAAK,OAAO,EAAe,KACtE,EAAkB,EAAM,GAAG,GAAG;IAE/B,CAAC,GAAO,EAAe,CAAC,EAGzB,kBAAC,OAAD;EAAK,WAAU;EAAuB,OAAO,EAAE,WAAW,uBAAuB;YAAjF;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAC/B,EAAG,kCAAkC,UAAU,CACzC;;MACT,kBAAC,QAAD;OAAM,WAAU;iBAAkB;OAAQ,CAAA;MAC1C,kBAAC,QAAD;OAAM,WAAU;iBAAyC,EAAK;OAAY,CAAA;MACtE;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG;MACD,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAmB,UAAU;QAAiB,QAAQ;QAAkB,CAAA;OACpE,CAAA;MACN,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU;OACV,WAAU;iBAJZ,CAME,kBAAC,GAAD,EAAW,WAAW,YAAY,IAAY,iBAAiB,MAAQ,CAAA,EACtE,EAAG,kCAAkC,UAAU,CACzC;;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,GAAgB,MAAM,CAAC,EAAE,EACzB,EAAgB,GAAM;;OAExB,WAAW,2BAA2B,IAAc,iCAAiC;OACrF,OAAM;iBAEN,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;OACvB,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,EAAgB,GAAK,EACrB,EAAe,GAAM;;OAEvB,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;OACxB,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;OACtB,CAAA;MACL;OACF;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,WAAW,EAAK,aAAa,EAAE;KAC/B,mBAAmB,EAAK;KACZ;KACC;KACb,CAAA;IACE,CAAA;GAIR,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACS;MACS;MAChB,cAAc;MACH;MACC;MACE;MACd,CAAA;KACE,CAAA,EAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAgB,MAAM;MAAc,QAAQ;MAAc,CAAA;KACtD,CAAA,CACF;;GAGN,kBAAC,GAAD;IACQ;IACN,MAAM;IACN,eAAe,EAAgB,GAAM;IACrC,QAAQ;IACR,CAAA;GACE"}
1
+ {"version":3,"file":"DocsEditPanel.js","names":[],"sources":["../../../src/components/docs/DocsEditPanel.tsx"],"sourcesContent":["import { useEffect, useState, type ReactNode } from 'react';\nimport { ArrowLeft, Settings, RefreshCw, History, Trash2 } from 'lucide-react';\nimport { DocsPageTree } from './DocsPageTree';\nimport { DocsPageEditor } from './DocsPageEditor';\nimport { DocsSettingsPanel } from './DocsSettingsPanel';\nimport { DocsCompileStatus } from './DocsCompileStatus';\nimport { DocsRevisionHistory } from './DocsRevisionHistory';\nimport type { DocsSite, DocsPage, DocsRevision } from './types';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface DocsEditPanelProps {\n site: DocsSite;\n pages: DocsPage[];\n onBack: () => void;\n onSavePage: (\n id: string,\n data: { title?: string; content?: string; slug?: string }\n ) => Promise<void>;\n onAddPage: (parentId?: string, category?: string) => void;\n pageAdding?: boolean;\n /** Omitted when the caller lacks docs_page:delete — hides the page delete control. */\n onDeletePage?: (pageId: string) => void;\n onCompile: () => void;\n onSaveSettings: (data: {\n name?: string;\n visibility?: 'PRIVATE' | 'PUBLIC';\n provider?: 'DOCUSAURUS' | 'MKDOCS' | 'VITEPRESS';\n providerConfig?: Record<string, unknown>;\n }) => Promise<void>;\n onRollback: (revisionId: string) => Promise<void>;\n /** Omitted when the caller lacks docs_site:delete — hides the Delete-site control. */\n onDeleteSite?: () => Promise<void>;\n rollingBack: string | null;\n compileRevision: DocsRevision | null;\n onPollRevision?: (id: string) => Promise<DocsRevision | null>;\n compiling: boolean;\n /** Generic grant-based Share affordance (owner-keyed; supplied by the host). */\n shareSlot?: ReactNode;\n}\n\nexport function DocsEditPanel({\n site,\n pages,\n onBack,\n onSavePage,\n onAddPage,\n pageAdding,\n onDeletePage,\n onCompile,\n onSaveSettings,\n onRollback,\n onDeleteSite,\n rollingBack,\n compileRevision,\n onPollRevision,\n compiling,\n shareSlot,\n}: DocsEditPanelProps) {\n const tr = useTr();\n const [selectedPageId, setSelectedPageId] = useState<string | null>(pages[0]?.id || null);\n const [showSettings, setShowSettings] = useState(false);\n const [showHistory, setShowHistory] = useState(false);\n\n const selectedPage = pages.find((p) => p.id === selectedPageId) || null;\n\n useEffect(() => {\n if (pages.length === 0) {\n if (selectedPageId) setSelectedPageId(null);\n return;\n }\n if (!selectedPageId || !pages.some((page) => page.id === selectedPageId)) {\n setSelectedPageId(pages[0].id);\n }\n }, [pages, selectedPageId]);\n\n return (\n <div className=\"flex flex-col h-full\" style={{ minHeight: 'calc(100vh - 380px)' }}>\n {/* Top bar */}\n <div className=\"flex items-center justify-between px-4 py-2 border-b border-border-default bg-bg-surface\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={onBack}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibecontrols.docs.edit.preview', 'Preview')}\n </button>\n <span className=\"text-text-muted\">|</span>\n <span className=\"text-sm font-medium text-text-primary\">{site.name}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n {shareSlot}\n <div className=\"relative\">\n <DocsCompileStatus revision={compileRevision} onPoll={onPollRevision} />\n </div>\n <button\n type=\"button\"\n onClick={onCompile}\n disabled={compiling}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n <RefreshCw className={`size-3.5 ${compiling ? 'animate-spin' : ''}`} />\n {tr('vibecontrols.docs.edit.compile', 'Compile')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowHistory((v) => !v);\n setShowSettings(false);\n }}\n className={`p-1.5 transition-colors ${showHistory ? 'text-action-primary-text-alt' : 'text-text-secondary hover:text-text-primary'}`}\n title=\"Build history\"\n >\n <History className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(true);\n setShowHistory(false);\n }}\n className=\"p-1.5 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Settings\"\n >\n <Settings className=\"size-4\" />\n </button>\n {onDeleteSite && (\n <button\n type=\"button\"\n onClick={onDeleteSite}\n className=\"p-1.5 text-text-muted hover:text-status-error-text transition-colors\"\n title=\"Delete site\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n )}\n </div>\n </div>\n\n {/* Revision history drawer */}\n {showHistory && (\n <div className=\"border-b border-border-default bg-bg-base max-h-64 overflow-y-auto\">\n <DocsRevisionHistory\n revisions={site.revisions ?? []}\n currentRevisionId={site.currentRevisionId}\n onRollback={onRollback}\n rollingBack={rollingBack}\n />\n </div>\n )}\n\n {/* Main content: sidebar + editor */}\n <div className=\"flex-1 grid grid-cols-[260px_1fr] min-h-0\">\n {/* Page tree sidebar */}\n <div className=\"border-r border-border-default overflow-hidden\">\n <DocsPageTree\n pages={pages}\n selectedPageId={selectedPageId}\n onSelectPage={setSelectedPageId}\n onAddPage={onAddPage}\n pageAdding={pageAdding}\n onDeletePage={onDeletePage}\n />\n </div>\n\n {/* Editor */}\n <div className=\"overflow-hidden\">\n <DocsPageEditor page={selectedPage} onSave={onSavePage} />\n </div>\n </div>\n\n {/* Settings panel */}\n <DocsSettingsPanel\n site={site}\n open={showSettings}\n onClose={() => setShowSettings(false)}\n onSave={onSaveSettings}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAwCA,SAAgB,EAAc,EAC5B,SACA,UACA,WACA,eACA,cACA,eACA,iBACA,cACA,mBACA,eACA,iBACA,gBACA,oBACA,mBACA,cACA,gBACqB;CACrB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAgB,KAAqB,EAAwB,EAAM,IAAI,MAAM,KAAK,EACnF,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAa,KAAkB,EAAS,GAAM,EAE/C,IAAe,EAAM,MAAM,MAAM,EAAE,OAAO,EAAe,IAAI;AAYnE,QAVA,QAAgB;AACd,MAAI,EAAM,WAAW,GAAG;AACtB,GAAI,KAAgB,EAAkB,KAAK;AAC3C;;AAEF,GAAI,CAAC,KAAkB,CAAC,EAAM,MAAM,MAAS,EAAK,OAAO,EAAe,KACtE,EAAkB,EAAM,GAAG,GAAG;IAE/B,CAAC,GAAO,EAAe,CAAC,EAGzB,kBAAC,OAAD;EAAK,WAAU;EAAuB,OAAO,EAAE,WAAW,uBAAuB;YAAjF;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAC/B,EAAG,kCAAkC,UAAU,CACzC;;MACT,kBAAC,QAAD;OAAM,WAAU;iBAAkB;OAAQ,CAAA;MAC1C,kBAAC,QAAD;OAAM,WAAU;iBAAyC,EAAK;OAAY,CAAA;MACtE;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG;MACD,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAmB,UAAU;QAAiB,QAAQ;QAAkB,CAAA;OACpE,CAAA;MACN,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU;OACV,WAAU;iBAJZ,CAME,kBAAC,GAAD,EAAW,WAAW,YAAY,IAAY,iBAAiB,MAAQ,CAAA,EACtE,EAAG,kCAAkC,UAAU,CACzC;;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,GAAgB,MAAM,CAAC,EAAE,EACzB,EAAgB,GAAM;;OAExB,WAAW,2BAA2B,IAAc,iCAAiC;OACrF,OAAM;iBAEN,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;OACvB,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,EAAgB,GAAK,EACrB,EAAe,GAAM;;OAEvB,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;OACxB,CAAA;MACR,KACC,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;OACtB,CAAA;MAEP;OACF;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,WAAW,EAAK,aAAa,EAAE;KAC/B,mBAAmB,EAAK;KACZ;KACC;KACb,CAAA;IACE,CAAA;GAIR,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACS;MACS;MAChB,cAAc;MACH;MACC;MACE;MACd,CAAA;KACE,CAAA,EAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAgB,MAAM;MAAc,QAAQ;MAAc,CAAA;KACtD,CAAA,CACF;;GAGN,kBAAC,GAAD;IACQ;IACN,MAAM;IACN,eAAe,EAAgB,GAAM;IACrC,QAAQ;IACR,CAAA;GACE"}
@@ -40,7 +40,7 @@ function f({ node: e, depth: t, selectedPageId: o, onSelectPage: s, onDeletePage
40
40
  className: "truncate flex-1",
41
41
  children: e.page.title
42
42
  }),
43
- /* @__PURE__ */ l("button", {
43
+ d && /* @__PURE__ */ l("button", {
44
44
  type: "button",
45
45
  onClick: (t) => {
46
46
  t.stopPropagation(), d(e.page.id);
@@ -1 +1 @@
1
- {"version":3,"file":"DocsPageTree.js","names":[],"sources":["../../../src/components/docs/DocsPageTree.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n FileText,\n FolderOpen,\n Plus,\n Trash2,\n ChevronRight,\n ChevronDown,\n Loader2,\n} from 'lucide-react';\nimport type { DocsPage } from './types';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface DocsPageTreeProps {\n pages: DocsPage[];\n selectedPageId: string | null;\n onSelectPage: (pageId: string) => void;\n onAddPage: (parentId?: string, category?: string) => void;\n pageAdding?: boolean;\n onDeletePage: (pageId: string) => void;\n}\n\ninterface TreeNode {\n page: DocsPage;\n children: TreeNode[];\n}\n\nfunction buildTree(pages: DocsPage[]): TreeNode[] {\n const map = new Map<string, TreeNode>();\n const roots: TreeNode[] = [];\n\n for (const page of pages) {\n map.set(page.id, { page, children: [] });\n }\n\n for (const page of pages) {\n const node = map.get(page.id)!;\n if (page.parentId && map.has(page.parentId)) {\n map.get(page.parentId)!.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n return roots;\n}\n\nfunction TreeItem({\n node,\n depth,\n selectedPageId,\n onSelectPage,\n onDeletePage,\n expandedIds,\n toggleExpand,\n}: {\n node: TreeNode;\n depth: number;\n selectedPageId: string | null;\n onSelectPage: (id: string) => void;\n onDeletePage: (id: string) => void;\n expandedIds: Set<string>;\n toggleExpand: (id: string) => void;\n}) {\n const isSelected = selectedPageId === node.page.id;\n const hasChildren = node.children.length > 0;\n const isExpanded = expandedIds.has(node.page.id);\n\n return (\n <div>\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => onSelectPage(node.page.id))();\n }\n }}\n className={`flex items-center gap-1 px-2 py-1.5 cursor-pointer rounded text-sm transition-colors group ${\n isSelected\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n style={{ paddingLeft: `${8 + depth * 16}px` }}\n onClick={() => onSelectPage(node.page.id)}\n >\n {hasChildren ? (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n toggleExpand(node.page.id);\n }}\n className=\"p-0.5 -ml-1\"\n >\n {isExpanded ? <ChevronDown className=\"size-3\" /> : <ChevronRight className=\"size-3\" />}\n </button>\n ) : (\n <span className=\"w-4\" />\n )}\n {node.page.category ? (\n <FolderOpen className=\"size-3.5 flex-shrink-0\" />\n ) : (\n <FileText className=\"size-3.5 flex-shrink-0\" />\n )}\n <span className=\"truncate flex-1\">{node.page.title}</span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onDeletePage(node.page.id);\n }}\n className=\"opacity-0 group-hover:opacity-100 p-0.5 text-text-muted hover:text-status-error-text transition-all\"\n title=\"Delete\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n {hasChildren && isExpanded && (\n <div>\n {node.children.map((child) => (\n <TreeItem\n key={child.page.id}\n node={child}\n depth={depth + 1}\n selectedPageId={selectedPageId}\n onSelectPage={onSelectPage}\n onDeletePage={onDeletePage}\n expandedIds={expandedIds}\n toggleExpand={toggleExpand}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport function DocsPageTree({\n pages,\n selectedPageId,\n onSelectPage,\n onAddPage,\n pageAdding,\n onDeletePage,\n}: DocsPageTreeProps) {\n const tr = useTr();\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(pages.map((p) => p.id)));\n\n const toggleExpand = (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n const tree = buildTree(pages);\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-border-default\">\n <span className=\"text-xs font-medium text-text-secondary uppercase tracking-wider\">\n {tr('vibecontrols.docs.pageTree.pages', 'Pages')}\n </span>\n <button\n type=\"button\"\n onClick={() => onAddPage()}\n disabled={pageAdding}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50\"\n title=\"Add page\"\n >\n {pageAdding ? <Loader2 className=\"size-4 animate-spin\" /> : <Plus className=\"size-4\" />}\n </button>\n </div>\n <div className=\"flex-1 overflow-y-auto py-1\">\n {tree.length > 0 ? (\n tree.map((node) => (\n <TreeItem\n key={node.page.id}\n node={node}\n depth={0}\n selectedPageId={selectedPageId}\n onSelectPage={onSelectPage}\n onDeletePage={onDeletePage}\n expandedIds={expandedIds}\n toggleExpand={toggleExpand}\n />\n ))\n ) : (\n <div className=\"p-4 text-center\">\n <p className=\"text-sm text-text-muted mb-2\">\n {tr('vibecontrols.docs.pageTree.noPages', 'No pages yet')}\n </p>\n <button\n type=\"button\"\n onClick={() => onAddPage()}\n className=\"text-sm text-action-primary-bg hover:underline\"\n >\n {tr('vibecontrols.docs.pageTree.addFirstPage', 'Add your first page')}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocsPageTree.js","names":[],"sources":["../../../src/components/docs/DocsPageTree.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n FileText,\n FolderOpen,\n Plus,\n Trash2,\n ChevronRight,\n ChevronDown,\n Loader2,\n} from 'lucide-react';\nimport type { DocsPage } from './types';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface DocsPageTreeProps {\n pages: DocsPage[];\n selectedPageId: string | null;\n onSelectPage: (pageId: string) => void;\n onAddPage: (parentId?: string, category?: string) => void;\n pageAdding?: boolean;\n /** Omitted when the caller lacks docs_page:delete — hides the delete control. */\n onDeletePage?: (pageId: string) => void;\n}\n\ninterface TreeNode {\n page: DocsPage;\n children: TreeNode[];\n}\n\nfunction buildTree(pages: DocsPage[]): TreeNode[] {\n const map = new Map<string, TreeNode>();\n const roots: TreeNode[] = [];\n\n for (const page of pages) {\n map.set(page.id, { page, children: [] });\n }\n\n for (const page of pages) {\n const node = map.get(page.id)!;\n if (page.parentId && map.has(page.parentId)) {\n map.get(page.parentId)!.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n return roots;\n}\n\nfunction TreeItem({\n node,\n depth,\n selectedPageId,\n onSelectPage,\n onDeletePage,\n expandedIds,\n toggleExpand,\n}: {\n node: TreeNode;\n depth: number;\n selectedPageId: string | null;\n onSelectPage: (id: string) => void;\n onDeletePage?: (id: string) => void;\n expandedIds: Set<string>;\n toggleExpand: (id: string) => void;\n}) {\n const isSelected = selectedPageId === node.page.id;\n const hasChildren = node.children.length > 0;\n const isExpanded = expandedIds.has(node.page.id);\n\n return (\n <div>\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => onSelectPage(node.page.id))();\n }\n }}\n className={`flex items-center gap-1 px-2 py-1.5 cursor-pointer rounded text-sm transition-colors group ${\n isSelected\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n style={{ paddingLeft: `${8 + depth * 16}px` }}\n onClick={() => onSelectPage(node.page.id)}\n >\n {hasChildren ? (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n toggleExpand(node.page.id);\n }}\n className=\"p-0.5 -ml-1\"\n >\n {isExpanded ? <ChevronDown className=\"size-3\" /> : <ChevronRight className=\"size-3\" />}\n </button>\n ) : (\n <span className=\"w-4\" />\n )}\n {node.page.category ? (\n <FolderOpen className=\"size-3.5 flex-shrink-0\" />\n ) : (\n <FileText className=\"size-3.5 flex-shrink-0\" />\n )}\n <span className=\"truncate flex-1\">{node.page.title}</span>\n {onDeletePage && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onDeletePage(node.page.id);\n }}\n className=\"opacity-0 group-hover:opacity-100 p-0.5 text-text-muted hover:text-status-error-text transition-all\"\n title=\"Delete\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n )}\n </div>\n {hasChildren && isExpanded && (\n <div>\n {node.children.map((child) => (\n <TreeItem\n key={child.page.id}\n node={child}\n depth={depth + 1}\n selectedPageId={selectedPageId}\n onSelectPage={onSelectPage}\n onDeletePage={onDeletePage}\n expandedIds={expandedIds}\n toggleExpand={toggleExpand}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport function DocsPageTree({\n pages,\n selectedPageId,\n onSelectPage,\n onAddPage,\n pageAdding,\n onDeletePage,\n}: DocsPageTreeProps) {\n const tr = useTr();\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(pages.map((p) => p.id)));\n\n const toggleExpand = (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n const tree = buildTree(pages);\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-border-default\">\n <span className=\"text-xs font-medium text-text-secondary uppercase tracking-wider\">\n {tr('vibecontrols.docs.pageTree.pages', 'Pages')}\n </span>\n <button\n type=\"button\"\n onClick={() => onAddPage()}\n disabled={pageAdding}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50\"\n title=\"Add page\"\n >\n {pageAdding ? <Loader2 className=\"size-4 animate-spin\" /> : <Plus className=\"size-4\" />}\n </button>\n </div>\n <div className=\"flex-1 overflow-y-auto py-1\">\n {tree.length > 0 ? (\n tree.map((node) => (\n <TreeItem\n key={node.page.id}\n node={node}\n depth={0}\n selectedPageId={selectedPageId}\n onSelectPage={onSelectPage}\n onDeletePage={onDeletePage}\n expandedIds={expandedIds}\n toggleExpand={toggleExpand}\n />\n ))\n ) : (\n <div className=\"p-4 text-center\">\n <p className=\"text-sm text-text-muted mb-2\">\n {tr('vibecontrols.docs.pageTree.noPages', 'No pages yet')}\n </p>\n <button\n type=\"button\"\n onClick={() => onAddPage()}\n className=\"text-sm text-action-primary-bg hover:underline\"\n >\n {tr('vibecontrols.docs.pageTree.addFirstPage', 'Add your first page')}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}