@burdenoff/microfe-vibecontrols 2026.905.3 → 2026.910.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.
- package/dist/components/agent-manager/AgentManagerMessageItem.js +1 -1
- package/dist/components/agent-manager/AgentManagerMessageItem.js.map +1 -1
- package/dist/components/assistant/AssistantMessageItem.js +1 -1
- package/dist/components/assistant/ProductMarkdown.js +30 -0
- package/dist/components/assistant/ProductMarkdown.js.map +1 -0
- package/dist/components/assistant/index.js +1 -1
- package/dist/providers/VibeControlsProvider.js +83 -79
- package/dist/providers/VibeControlsProvider.js.map +1 -1
- package/dist/services/assistantApi.js.map +1 -1
- package/dist/services/speech/index.js +2 -0
- package/package.json +2 -2
- package/dist/components/assistant/AssistantMarkdownRenderer.js +0 -205
- package/dist/components/assistant/AssistantMarkdownRenderer.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ProductMarkdown as t } from "../assistant/ProductMarkdown.js";
|
|
3
3
|
import { relativeTime as n } from "../../utils/relativeTime.js";
|
|
4
4
|
import { useState as r } from "react";
|
|
5
5
|
import { Bookmark as i, BookmarkCheck as a, Bot as o, Check as s, Copy as c, User as l } from "lucide-react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentManagerMessageItem.js","names":[],"sources":["../../../src/components/agent-manager/AgentManagerMessageItem.tsx"],"sourcesContent":["/**\n * Single message bubble for the Agent Manager chat.\n *\n * User messages are right-aligned; assistant messages are left-aligned.\n * Uses
|
|
1
|
+
{"version":3,"file":"AgentManagerMessageItem.js","names":[],"sources":["../../../src/components/agent-manager/AgentManagerMessageItem.tsx"],"sourcesContent":["/**\n * Single message bubble for the Agent Manager chat.\n *\n * User messages are right-aligned; assistant messages are left-aligned.\n * Uses the SHARED assistant markdown renderer (via ProductMarkdown) for\n * rich content in assistant messages, so links here follow the same origin\n * policy as the assistant panel's.\n * Supports optional search query highlighting.\n */\n\nimport { useState, type FC, type ReactNode } from 'react';\nimport { Bot, Bookmark, BookmarkCheck, Copy, Check, User } from 'lucide-react';\nimport { ProductMarkdown } from '@/components/assistant/ProductMarkdown';\nimport { relativeTime } from '@/utils/relativeTime';\nimport type { AgentManagerMessage } from '@/types/agentManager';\nimport { useTr } from '../../shared/hooks/useTr';\n\nconst ANSI_ESCAPE_RE = new RegExp(\n `${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\\\[[0-?]*[ -/]*[@-~])`,\n 'g'\n);\n\nfunction stripAnsi(value: string): string {\n return value.replace(ANSI_ESCAPE_RE, '');\n}\n\n/** Highlight occurrences of `query` within `text`. */\nfunction highlightText(text: string, query: string): ReactNode {\n if (!query) return text;\n\n const lowerText = text.toLowerCase();\n const lowerQuery = query.toLowerCase();\n const parts: ReactNode[] = [];\n let lastIndex = 0;\n let matchIndex = lowerText.indexOf(lowerQuery, lastIndex);\n\n while (matchIndex !== -1) {\n if (matchIndex > lastIndex) {\n parts.push(text.slice(lastIndex, matchIndex));\n }\n parts.push(\n <mark\n key={matchIndex}\n className=\"bg-status-warning-bg-subtle text-status-warning-text rounded-sm px-0.5\"\n >\n {text.slice(matchIndex, matchIndex + query.length)}\n </mark>\n );\n lastIndex = matchIndex + query.length;\n matchIndex = lowerText.indexOf(lowerQuery, lastIndex);\n }\n\n if (lastIndex < text.length) {\n parts.push(text.slice(lastIndex));\n }\n\n return parts.length > 0 ? <>{parts}</> : text;\n}\n\ninterface AgentManagerMessageItemProps {\n message: AgentManagerMessage;\n searchQuery?: string;\n isBookmarked?: boolean;\n onToggleBookmark?: (messageId: string) => void;\n flashMessageId?: string | null;\n}\n\nexport const AgentManagerMessageItem: FC<AgentManagerMessageItemProps> = ({\n message,\n searchQuery = '',\n isBookmarked = false,\n onToggleBookmark,\n flashMessageId,\n}) => {\n const [copied, setCopied] = useState(false);\n const [hovered, setHovered] = useState(false);\n const isUser = message.role === 'user';\n const tr = useTr();\n const displayContent = stripAnsi(message.content);\n const isFlashing = flashMessageId === message.id;\n\n const handleCopy = () => {\n navigator.clipboard.writeText(displayContent);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleToggleBookmark = () => {\n if (onToggleBookmark) onToggleBookmark(message.id);\n };\n\n const totalTokens = message.tokens ? message.tokens.input + message.tokens.output : null;\n\n return (\n <div\n data-message-id={message.id}\n className={`flex gap-2.5 scroll-mt-16 transition-shadow duration-300 ${\n isUser ? 'flex-row-reverse' : 'flex-row'\n } ${isFlashing ? 'ring-2 ring-status-warning-bg/60 rounded-lg' : ''}`}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n >\n {/* Avatar */}\n <div\n className={`flex-shrink-0 size-6 rounded-full flex items-center justify-center mt-0.5 ${\n isUser ? 'bg-action-primary-bg' : 'bg-bg-elevated border border-border-default'\n }`}\n >\n {isUser ? (\n <User className=\"size-3.5 text-action-primary-text\" />\n ) : (\n <Bot className=\"size-3.5 text-text-secondary\" />\n )}\n </div>\n\n {/* Message bubble */}\n <div\n className={`relative max-w-[85%] rounded-lg px-3 py-2 ${\n isUser\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-elevated border border-border-subtle'\n }`}\n >\n {/* Content */}\n {isUser ? (\n <p className=\"text-xs whitespace-pre-wrap leading-relaxed break-words\">\n {searchQuery ? highlightText(displayContent, searchQuery) : displayContent}\n </p>\n ) : searchQuery ? (\n <p className=\"text-xs whitespace-pre-wrap leading-relaxed break-words text-text-primary\">\n {highlightText(displayContent, searchQuery)}\n </p>\n ) : (\n <ProductMarkdown content={displayContent} />\n )}\n\n {/* Footer: timestamp, tokens, bookmark, copy */}\n <div\n className={`flex items-center gap-2 text-[10px] mt-1 ${\n isUser ? 'text-action-primary-text/60' : 'text-text-muted'\n }`}\n >\n <span>{relativeTime(message.timestamp)}</span>\n\n {totalTokens !== null && !isUser && (\n <span>\n {totalTokens} {tr('vibecontrols.agentManager.messageItem.tokens', 'tokens')}\n </span>\n )}\n\n {message.sdk && (\n <span className=\"px-1 py-0.5 rounded bg-bg-elevated text-text-muted uppercase font-mono text-[9px]\">\n {message.sdk}\n </span>\n )}\n\n {message.model && !isUser && <span>{message.model}</span>}\n\n {/* Bookmark + Copy buttons on hover (or when bookmarked) */}\n {(hovered || isBookmarked) && onToggleBookmark && (\n <button\n type=\"button\"\n onClick={handleToggleBookmark}\n className={`ml-auto p-0.5 rounded transition-colors ${\n isUser ? 'hover:bg-action-primary-bg-hover' : 'hover:bg-bg-sunken'\n }`}\n aria-label={\n isBookmarked\n ? tr('vibecontrols.agentManager.messageItem.removeBookmark', 'Remove bookmark')\n : tr('vibecontrols.agentManager.messageItem.bookmarkMessage', 'Bookmark message')\n }\n title={\n isBookmarked\n ? tr('vibecontrols.agentManager.messageItem.removeBookmark', 'Remove bookmark')\n : tr('vibecontrols.agentManager.messageItem.bookmarkMessage', 'Bookmark message')\n }\n data-testid=\"agent-manager-bookmark-toggle\"\n >\n {isBookmarked ? (\n <BookmarkCheck className=\"size-3 text-status-warning-text\" />\n ) : (\n <Bookmark className=\"size-3\" />\n )}\n </button>\n )}\n\n {hovered && (\n <button\n type=\"button\"\n onClick={handleCopy}\n className={`${onToggleBookmark ? '' : 'ml-auto'} p-0.5 rounded transition-colors ${\n isUser ? 'hover:bg-action-primary-bg-hover' : 'hover:bg-bg-sunken'\n }`}\n aria-label={tr('vibecontrols.agentManager.messageItem.copyMessage', 'Copy message')}\n title={tr('vibecontrols.agentManager.messageItem.copyMessage', 'Copy message')}\n >\n {copied ? (\n <Check className=\"size-3 text-status-success-text\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useNavigate as t } from "react-router";
|
|
3
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
4
|
+
import { AssistantI18nProvider as r } from "@burdenoff/fe-libs/shared/assistant/i18n";
|
|
5
|
+
import { AssistantMarkdownRenderer as i, AssistantNavigateProvider as a } from "@burdenoff/fe-libs/shared/assistant/ui/AssistantMarkdownRenderer";
|
|
6
|
+
import { isShellLessRoute as o } from "@burdenoff/fe-libs/shared/config/shellRoutes";
|
|
7
|
+
//#region src/components/assistant/ProductMarkdown.tsx
|
|
8
|
+
var s = ({ content: s }) => {
|
|
9
|
+
let c = t();
|
|
10
|
+
return /* @__PURE__ */ n("div", {
|
|
11
|
+
className: "text-xs",
|
|
12
|
+
children: /* @__PURE__ */ n(r, {
|
|
13
|
+
namespace: "vibecontrols",
|
|
14
|
+
children: /* @__PURE__ */ n(a, {
|
|
15
|
+
value: e((e) => {
|
|
16
|
+
if (o(e.split(/[?#]/)[0] || "/")) {
|
|
17
|
+
window.open(e, "_blank", "noopener,noreferrer");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
c(e);
|
|
21
|
+
}, [c]),
|
|
22
|
+
children: /* @__PURE__ */ n(i, { content: s })
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { s as ProductMarkdown };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=ProductMarkdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductMarkdown.js","names":[],"sources":["../../../src/components/assistant/ProductMarkdown.tsx"],"sourcesContent":["/**\n * VibeControls markdown — rendered by the SHARED assistant renderer.\n *\n * ★★ This REPLACES a 335-line hand-rolled regex renderer (deleted in this change) that forced\n * `target=\"_blank\" rel=\"noopener noreferrer\"` onto EVERY link with no origin\n * check and no scheme allow-list. Agent Manager content is model output relayed\n * from a user-registered remote agent, so it is doubly untrusted — and an in-app\n * path in it opened a second copy of the whole app in a new tab, the exact\n * behaviour fe-libs#202 removed from the shared assistant. This page never\n * inherited that fix because it never used the shared renderer.\n *\n * The shared renderer brings, for free:\n * - origin-based internal/external classification, so an in-app path routes\n * in-app and only genuinely external links get `_blank` + `noopener`;\n * - a real CommonMark+GFM parser with rehype-sanitize, in place of a regex\n * that emitted `<li>` with no `<ul>` parent, one `<p>` per SOURCE line, and\n * dropped empty table cells (shifting every later column out of alignment);\n * - no lookbehind regex, which threw at PARSE time on Safari < 16.4 and sat in\n * the app's ENTRY chunk.\n *\n * ★ The i18n keys are unchanged. fe-libs calls `tr('markdown.copyCode', …)`,\n * which under `namespace=\"vibecontrols\"` resolves to\n * `vibecontrols.assistant.markdown.copyCode` — the exact key the fork used, so\n * every already-seeded translation still lands.\n *\n * ★ Used by BOTH the Agent Manager bubble and the (currently unmounted)\n * assistant bubble, so the two surfaces cannot diverge again.\n *\n * ★ `text-xs` is supplied here because the shared renderer deliberately inherits\n * its font size from the message bubble that mounts it.\n */\nimport { useCallback, type FC } from 'react';\nimport { useNavigate } from 'react-router';\n\nimport { AssistantI18nProvider } from '@burdenoff/fe-libs/shared/assistant/i18n';\nimport {\n AssistantMarkdownRenderer,\n AssistantNavigateProvider,\n} from '@burdenoff/fe-libs/shared/assistant/ui/AssistantMarkdownRenderer';\nimport { isShellLessRoute } from '@burdenoff/fe-libs/shared/config/shellRoutes';\n\nexport interface ProductMarkdownProps {\n content: string;\n}\n\nexport const ProductMarkdown: FC<ProductMarkdownProps> = ({ content }) => {\n const navigate = useNavigate();\n\n /**\n * ★ fe-libs hands over a router-ready PATH, already resolved from the href and\n * proven same-origin — an off-origin link never reaches here, it opens in a new\n * tab. So this is a plain in-app route change.\n *\n * ★★ Except for a SHELL-LESS destination. `ConditionalShell` renders those bare\n * (the app's `App.tsx`), which unmounts `AppShell` — and this page with it, chat\n * state included. Those are unauthenticated pages that bring their own chrome,\n * so a new tab is what a link to one wants anyway. Same fe-libs predicate the\n * shell itself uses, so the two cannot disagree about what \"shell-less\" means.\n */\n const navigateFromMarkdown = useCallback(\n (path: string) => {\n // fe-libs sends `pathname + search + hash`; the predicate matches a pathname.\n const pathname = path.split(/[?#]/)[0] || '/';\n if (isShellLessRoute(pathname)) {\n window.open(path, '_blank', 'noopener,noreferrer');\n return;\n }\n navigate(path);\n },\n [navigate]\n );\n\n return (\n <div className=\"text-xs\">\n <AssistantI18nProvider namespace=\"vibecontrols\">\n <AssistantNavigateProvider value={navigateFromMarkdown}>\n <AssistantMarkdownRenderer content={content} />\n </AssistantNavigateProvider>\n </AssistantI18nProvider>\n </div>\n );\n};\n"],"mappings":";;;;;;;AA6CA,IAAa,KAA6C,EAAE,iBAAc;CACxE,IAAM,IAAW,GAAa;AA0B9B,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GAAuB,WAAU;aAC/B,kBAAC,GAAD;IAA2B,OAhBJ,GAC1B,MAAiB;AAGhB,SAAI,EADa,EAAK,MAAM,OAAO,CAAC,MAAM,IACZ,EAAE;AAC9B,aAAO,KAAK,GAAM,UAAU,sBAAsB;AAClD;;AAEF,OAAS,EAAK;OAEhB,CAAC,EAAS,CACX;cAMO,kBAAC,GAAD,EAAoC,YAAW,CAAA;IACrB,CAAA;GACN,CAAA;EACpB,CAAA"}
|
|
@@ -2,24 +2,25 @@ import { buildVibeControlsCache as e } from "../lib/apolloCache.js";
|
|
|
2
2
|
import { getMainDefinition as t } from "../node_modules/@apollo/client/utilities/internal/getMainDefinition.js";
|
|
3
3
|
import { CombinedGraphQLErrors as n } from "../node_modules/@apollo/client/errors/CombinedGraphQLErrors.js";
|
|
4
4
|
import { BatchHttpLink as r } from "../node_modules/@apollo/client/link/batch-http/batchHttpLink.js";
|
|
5
|
-
import i from "
|
|
6
|
-
import
|
|
5
|
+
import { configureSpeechEngine as i } from "../services/speech/index.js";
|
|
6
|
+
import a from "./AgentEventsProvider.js";
|
|
7
|
+
import { QUOTA_EXHAUSTED_EVENT as o, inferQuotaKindFromOperation as ee } from "../utils/quotaUtils.js";
|
|
7
8
|
import { QuotaExhaustedProvider as s } from "./QuotaExhaustedProvider.js";
|
|
8
9
|
import c from "../hooks/useInitialDataLoader.js";
|
|
9
|
-
import { createContext as l, use as u, useCallback as d, useEffect as f, useMemo as
|
|
10
|
-
import { MemoryRouter as
|
|
11
|
-
import { ApolloClient as
|
|
12
|
-
import { onError as
|
|
13
|
-
import { setContext as
|
|
14
|
-
import { ApolloProvider as
|
|
15
|
-
import { MutationCache as
|
|
16
|
-
import { getStoredAuthToken as
|
|
17
|
-
import { getStoredWorkspaceId as
|
|
18
|
-
import { PermissionProvider as
|
|
19
|
-
import { isAuthNetworkError as
|
|
20
|
-
import { Fragment as
|
|
10
|
+
import { createContext as l, use as u, useCallback as d, useEffect as f, useMemo as p, useRef as m, useState as h } from "react";
|
|
11
|
+
import { MemoryRouter as te, useNavigate as g } from "react-router";
|
|
12
|
+
import { ApolloClient as _, ApolloLink as v, Observable as y, from as b } from "@apollo/client";
|
|
13
|
+
import { onError as ne } from "@apollo/client/link/error";
|
|
14
|
+
import { setContext as re } from "@apollo/client/link/context";
|
|
15
|
+
import { ApolloProvider as x } from "@apollo/client/react";
|
|
16
|
+
import { MutationCache as S, QueryClient as C, QueryClientProvider as w } from "@tanstack/react-query";
|
|
17
|
+
import { getStoredAuthToken as ie, getStoredWorkspaceToken as ae, isWorkspaceTokenValidForContext as oe } from "@burdenoff/fe-libs/shared/graphql";
|
|
18
|
+
import { getStoredWorkspaceId as se } from "@burdenoff/fe-libs/shared/graphql/fetch";
|
|
19
|
+
import { PermissionProvider as ce } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
20
|
+
import { isAuthNetworkError as le } from "@burdenoff/fe-libs/shared/utils";
|
|
21
|
+
import { Fragment as T, jsx as E } from "react/jsx-runtime";
|
|
21
22
|
//#region src/providers/VibeControlsProvider.tsx
|
|
22
|
-
function
|
|
23
|
+
function D() {
|
|
23
24
|
try {
|
|
24
25
|
let e = sessionStorage.getItem("burdenoff-active-context-project");
|
|
25
26
|
if (e) return e;
|
|
@@ -31,27 +32,27 @@ function se() {
|
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
function
|
|
35
|
+
function O(e = []) {
|
|
35
36
|
return e.some((e) => {
|
|
36
37
|
let t = typeof e.extensions?.code == "string" ? e.extensions.code : "", n = typeof e.extensions?.errorCode == "string" ? e.extensions.errorCode : "", r = (e.message ?? "").toLowerCase();
|
|
37
38
|
return t === "UNAUTHENTICATED" || n === "TOKEN_EXPIRED" || r.includes("token_expired") || r.includes("token expired") || r.includes("session expired") || r.includes("unauthorized") || r.includes("unauthenticated") || r.includes("authentication required") || r.includes("workspace context missing");
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
|
-
function
|
|
41
|
+
function k(e) {
|
|
41
42
|
let n = t(e);
|
|
42
43
|
return n.kind === "OperationDefinition" && n.operation === "mutation";
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
+
function A(e) {
|
|
45
46
|
let t = e.errors;
|
|
46
47
|
return Array.isArray(t) && t.length > 0;
|
|
47
48
|
}
|
|
48
|
-
function
|
|
49
|
-
return new
|
|
50
|
-
let r =
|
|
51
|
-
return new
|
|
49
|
+
function j(e) {
|
|
50
|
+
return new v((t, n) => {
|
|
51
|
+
let r = k(t.query) && t.getContext().autoRefreshAfterMutation !== !1 && t.getContext().skipMutationAutoRefresh !== !0;
|
|
52
|
+
return new y((i) => {
|
|
52
53
|
let a = n(t).subscribe({
|
|
53
54
|
next: (t) => {
|
|
54
|
-
i.next(t), !(!r ||
|
|
55
|
+
i.next(t), !(!r || A(t)) && e()?.refetchQueries({ include: "active" });
|
|
55
56
|
},
|
|
56
57
|
error: (e) => {
|
|
57
58
|
i.error(e);
|
|
@@ -66,18 +67,18 @@ function M(e) {
|
|
|
66
67
|
});
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
|
-
function
|
|
70
|
+
function M(e = []) {
|
|
70
71
|
return e.find((e) => {
|
|
71
72
|
let t = typeof e.extensions?.code == "string" ? e.extensions.code : "", n = typeof e.extensions?.errorCode == "string" ? e.extensions.errorCode : "", r = (e.message ?? "").toLowerCase();
|
|
72
73
|
return t === "QUOTA_EXHAUSTED" || n === "QUOTA_EXHAUSTED" || r.includes("quota_exhausted") || r.includes("quota exhausted");
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
|
-
var
|
|
76
|
-
let e = u(
|
|
76
|
+
var N = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE__ */ E(T, { children: t })), P = l(null), F = () => {
|
|
77
|
+
let e = u(P);
|
|
77
78
|
if (!e) throw Error("useVibeControls must be used within VibeControlsProvider");
|
|
78
79
|
return e;
|
|
79
|
-
},
|
|
80
|
-
let { navigate: e, basePath: t } =
|
|
80
|
+
}, I = () => {
|
|
81
|
+
let { navigate: e, basePath: t } = F(), n = g();
|
|
81
82
|
return d((r) => {
|
|
82
83
|
let i = t && t !== "/" ? `${t}${r}` : r;
|
|
83
84
|
e ? e(i) : n(i);
|
|
@@ -86,10 +87,10 @@ var P = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE_
|
|
|
86
87
|
t,
|
|
87
88
|
n
|
|
88
89
|
]);
|
|
89
|
-
},
|
|
90
|
-
let [
|
|
91
|
-
let e = new
|
|
92
|
-
mutationCache: new
|
|
90
|
+
}, L = ({ children: t, basePath: c, navigate: l, currentUser: u, workspaceId: g, organizationId: v, projectId: y, tenantId: T, apiGatewayUrl: k, authToken: A, workspaceToken: F, onAgentSelect: I, onSessionStart: L, onVibeActivate: R, defaultView: z, registerNavItems: B, registerFooterItems: V, onAuthError: H, ttsEndpoint: U }) => {
|
|
91
|
+
let [ue] = h(() => {
|
|
92
|
+
let e = new C({
|
|
93
|
+
mutationCache: new S({ onSuccess: () => {
|
|
93
94
|
e.invalidateQueries();
|
|
94
95
|
} }),
|
|
95
96
|
defaultOptions: { queries: {
|
|
@@ -98,43 +99,46 @@ var P = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE_
|
|
|
98
99
|
} }
|
|
99
100
|
});
|
|
100
101
|
return e;
|
|
101
|
-
}), W =
|
|
102
|
+
}), W = m(A), G = m(F), K = m(g), q = m(v), J = m(T), Y = m(y), X = m(H);
|
|
102
103
|
f(() => {
|
|
103
104
|
W.current = A;
|
|
104
|
-
}, [A]),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
}, [A]), i({
|
|
106
|
+
ttsEndpoint: U,
|
|
107
|
+
getHeaders: d(() => W.current ? { authorization: `Bearer ${W.current}` } : {}, [])
|
|
108
|
+
}), f(() => {
|
|
109
|
+
G.current = F;
|
|
110
|
+
}, [F]), f(() => {
|
|
111
|
+
K.current = g;
|
|
110
112
|
}, [g]), f(() => {
|
|
111
|
-
|
|
113
|
+
q.current = v;
|
|
112
114
|
}, [v]), f(() => {
|
|
113
|
-
|
|
114
|
-
}, [
|
|
115
|
+
J.current = T;
|
|
116
|
+
}, [T]), f(() => {
|
|
117
|
+
Y.current = y;
|
|
118
|
+
}, [y]), f(() => {
|
|
115
119
|
X.current = H;
|
|
116
120
|
}, [H]);
|
|
117
|
-
let
|
|
118
|
-
let t =
|
|
121
|
+
let de = p(() => {
|
|
122
|
+
let t = ne(({ error: e, operation: t }) => {
|
|
119
123
|
if (n.is(e)) {
|
|
120
|
-
if (
|
|
124
|
+
if (O(e.errors)) {
|
|
121
125
|
console.warn("[VibeControls] Auth error detected — notifying host shell"), X.current?.();
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
|
-
let n =
|
|
128
|
+
let n = M(e.errors);
|
|
125
129
|
if (n) {
|
|
126
130
|
let e = {
|
|
127
|
-
kind:
|
|
131
|
+
kind: ee(t?.operationName),
|
|
128
132
|
message: n.message,
|
|
129
133
|
operationName: t?.operationName ?? null
|
|
130
134
|
};
|
|
131
|
-
window.dispatchEvent(new CustomEvent(
|
|
135
|
+
window.dispatchEvent(new CustomEvent(o, { detail: e }));
|
|
132
136
|
return;
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
|
-
e instanceof Error &&
|
|
136
|
-
}), i =
|
|
137
|
-
let n = W.current ??
|
|
139
|
+
e instanceof Error && le(e) && (console.warn("[VibeControls] Auth network error — notifying host shell"), X.current?.());
|
|
140
|
+
}), i = re((e, { headers: t }) => {
|
|
141
|
+
let n = W.current ?? ie(), r = K.current || se(), i = Y.current || D(), a = G.current ?? ae(), o = oe(a, {
|
|
138
142
|
workspaceId: r,
|
|
139
143
|
organizationId: q.current,
|
|
140
144
|
tenantId: J.current,
|
|
@@ -147,17 +151,17 @@ var P = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE_
|
|
|
147
151
|
...r ? { "x-workspace-id": r } : {},
|
|
148
152
|
...i ? { "x-project-id": i } : {}
|
|
149
153
|
} };
|
|
150
|
-
}),
|
|
151
|
-
uri:
|
|
154
|
+
}), a = new r({
|
|
155
|
+
uri: k ? `${k}/workspaces/graphql` : "/workspaces/graphql",
|
|
152
156
|
batchMax: 10,
|
|
153
157
|
batchInterval: 10
|
|
154
|
-
}),
|
|
155
|
-
return
|
|
156
|
-
link:
|
|
158
|
+
}), s = null;
|
|
159
|
+
return s = new _({
|
|
160
|
+
link: b([
|
|
157
161
|
t,
|
|
158
|
-
|
|
162
|
+
j(() => s),
|
|
159
163
|
i,
|
|
160
|
-
|
|
164
|
+
a
|
|
161
165
|
]),
|
|
162
166
|
cache: e(),
|
|
163
167
|
defaultOptions: {
|
|
@@ -168,18 +172,18 @@ var P = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE_
|
|
|
168
172
|
query: { errorPolicy: "all" },
|
|
169
173
|
mutate: { errorPolicy: "all" }
|
|
170
174
|
}
|
|
171
|
-
}),
|
|
172
|
-
}, [
|
|
175
|
+
}), s;
|
|
176
|
+
}, [k]), Z = {
|
|
173
177
|
basePath: c,
|
|
174
178
|
navigate: l,
|
|
175
179
|
currentUser: u,
|
|
176
|
-
workspaceId:
|
|
177
|
-
organizationId:
|
|
178
|
-
projectId:
|
|
179
|
-
tenantId:
|
|
180
|
-
apiGatewayUrl:
|
|
180
|
+
workspaceId: g,
|
|
181
|
+
organizationId: v,
|
|
182
|
+
projectId: y,
|
|
183
|
+
tenantId: T,
|
|
184
|
+
apiGatewayUrl: k,
|
|
181
185
|
authToken: A,
|
|
182
|
-
workspaceToken:
|
|
186
|
+
workspaceToken: F,
|
|
183
187
|
onAgentSelect: I,
|
|
184
188
|
onSessionStart: L,
|
|
185
189
|
onVibeActivate: R,
|
|
@@ -187,34 +191,34 @@ var P = ({ workspaceId: e, children: t }) => (c({ workspaceId: e }), /* @__PURE_
|
|
|
187
191
|
registerNavItems: B,
|
|
188
192
|
registerFooterItems: V,
|
|
189
193
|
onAuthError: H
|
|
190
|
-
}, Q = /* @__PURE__ */
|
|
191
|
-
workspaceId:
|
|
192
|
-
children: /* @__PURE__ */
|
|
193
|
-
scopeId:
|
|
194
|
+
}, Q = /* @__PURE__ */ E(s, { children: typeof window < "u" && /^\/vibecontrols\/(share|deck)\//.test(window.location.pathname) ? t : /* @__PURE__ */ E(N, {
|
|
195
|
+
workspaceId: g,
|
|
196
|
+
children: /* @__PURE__ */ E(ce, {
|
|
197
|
+
scopeId: g ?? null,
|
|
194
198
|
gateway: "workspace",
|
|
195
|
-
children: /* @__PURE__ */
|
|
199
|
+
children: /* @__PURE__ */ E(a, {
|
|
196
200
|
toastPosition: "bottom-right",
|
|
197
201
|
autoDismissDelay: 5e3,
|
|
198
202
|
maxVisibleToasts: 3,
|
|
199
203
|
children: t
|
|
200
204
|
})
|
|
201
205
|
})
|
|
202
|
-
}) }), $ = /* @__PURE__ */
|
|
203
|
-
client:
|
|
204
|
-
children: c ? /* @__PURE__ */
|
|
206
|
+
}) }), $ = /* @__PURE__ */ E(w, {
|
|
207
|
+
client: ue,
|
|
208
|
+
children: c ? /* @__PURE__ */ E(P.Provider, {
|
|
205
209
|
value: Z,
|
|
206
210
|
children: Q
|
|
207
|
-
}) : /* @__PURE__ */
|
|
208
|
-
client:
|
|
209
|
-
children: /* @__PURE__ */
|
|
211
|
+
}) : /* @__PURE__ */ E(x, {
|
|
212
|
+
client: de,
|
|
213
|
+
children: /* @__PURE__ */ E(P.Provider, {
|
|
210
214
|
value: Z,
|
|
211
215
|
children: Q
|
|
212
216
|
})
|
|
213
217
|
})
|
|
214
218
|
});
|
|
215
|
-
return c ? $ : /* @__PURE__ */
|
|
219
|
+
return c ? $ : /* @__PURE__ */ E(te, { children: $ });
|
|
216
220
|
};
|
|
217
221
|
//#endregion
|
|
218
|
-
export {
|
|
222
|
+
export { L as VibeControlsProvider, F as useVibeControls, I as useVibeNavigate };
|
|
219
223
|
|
|
220
224
|
//# sourceMappingURL=VibeControlsProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VibeControlsProvider.js","names":[],"sources":["../../src/providers/VibeControlsProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, use, useMemo, useCallback, useRef, useEffect, useState } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router';\nimport { ApolloClient, ApolloLink, type FetchResult, Observable, from } from '@apollo/client';\nimport { buildVibeControlsCache } from '@/lib/apolloCache';\nimport { BatchHttpLink } from '@apollo/client/link/batch-http';\nimport { onError } from '@apollo/client/link/error';\nimport { CombinedGraphQLErrors } from '@apollo/client/errors';\nimport { setContext } from '@apollo/client/link/context';\nimport { ApolloProvider } from '@apollo/client/react';\nimport { getMainDefinition } from '@apollo/client/utilities';\nimport { MutationCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n getStoredAuthToken,\n getStoredWorkspaceToken,\n isWorkspaceTokenValidForContext,\n} from '@burdenoff/fe-libs/shared/graphql';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { isAuthNetworkError } from '@burdenoff/fe-libs/shared/utils';\nimport type { VibeControlsRootProps } from '../types';\nimport { AgentEventsProvider } from './AgentEventsProvider';\nimport { QuotaExhaustedProvider } from './QuotaExhaustedProvider';\nimport { useInitialDataLoader } from '../hooks/useInitialDataLoader';\nimport {\n QUOTA_EXHAUSTED_EVENT,\n inferQuotaKindFromOperation,\n type QuotaExhaustedEventDetail,\n} from '../utils/quotaUtils';\n\ntype GraphQLErrorLike = { message?: string; extensions?: Record<string, unknown> };\n\nfunction getStoredProjectId(): string | null {\n try {\n const fromSession = sessionStorage.getItem('burdenoff-active-context-project');\n if (fromSession) return fromSession;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n const profileContext = activeProfileId\n ? localStorage.getItem(`bf-p-${activeProfileId}-context`)\n : null;\n if (!profileContext) return null;\n\n const parsed = JSON.parse(profileContext) as { projectId?: unknown };\n return typeof parsed.projectId === 'string' && parsed.projectId.length > 0\n ? parsed.projectId\n : null;\n } catch {\n return null;\n }\n}\n\nfunction hasSessionAuthErrors(errors: ReadonlyArray<GraphQLErrorLike> = []): boolean {\n return errors.some((error) => {\n const code = typeof error.extensions?.code === 'string' ? error.extensions.code : '';\n const errorCode =\n typeof error.extensions?.errorCode === 'string' ? error.extensions.errorCode : '';\n const message = (error.message ?? '').toLowerCase();\n\n return (\n code === 'UNAUTHENTICATED' ||\n errorCode === 'TOKEN_EXPIRED' ||\n message.includes('token_expired') ||\n message.includes('token expired') ||\n message.includes('session expired') ||\n message.includes('unauthorized') ||\n message.includes('unauthenticated') ||\n message.includes('authentication required') ||\n message.includes('workspace context missing')\n );\n });\n}\n\nfunction isMutationOperation(query: Parameters<typeof getMainDefinition>[0]): boolean {\n const definition = getMainDefinition(query);\n return definition.kind === 'OperationDefinition' && definition.operation === 'mutation';\n}\n\nfunction hasGraphQLResultErrors(result: FetchResult<Record<string, unknown>>): boolean {\n const errors = result.errors;\n return Array.isArray(errors) && errors.length > 0;\n}\n\nfunction createMutationAutoRefreshLink(\n getClient: () => InstanceType<typeof ApolloClient> | null\n): ApolloLink {\n return new ApolloLink((operation, forward) => {\n const shouldRefreshAfterMutation =\n isMutationOperation(operation.query) &&\n operation.getContext().autoRefreshAfterMutation !== false &&\n operation.getContext().skipMutationAutoRefresh !== true;\n\n return new Observable<FetchResult<Record<string, unknown>>>((observer) => {\n const subscription = forward(operation).subscribe({\n next: (result) => {\n observer.next(result as FetchResult<Record<string, unknown>>);\n if (!shouldRefreshAfterMutation || hasGraphQLResultErrors(result)) return;\n\n void getClient()?.refetchQueries({ include: 'active' });\n },\n error: (error: unknown) => {\n observer.error(error);\n },\n complete: () => {\n observer.complete();\n },\n });\n\n return () => {\n subscription.unsubscribe();\n };\n });\n });\n}\n\nfunction findQuotaExhaustedError(\n errors: ReadonlyArray<GraphQLErrorLike> = []\n): GraphQLErrorLike | undefined {\n return errors.find((error) => {\n const code = typeof error.extensions?.code === 'string' ? error.extensions.code : '';\n const errorCode =\n typeof error.extensions?.errorCode === 'string' ? error.extensions.errorCode : '';\n // Match both the constant form and the human-readable phrase, case\n // insensitively — mirrors what `isQuotaExhaustedError` accepts at the\n // per-page level so the link doesn't miss any quota error the rest\n // of the code base already recognises.\n const message = (error.message ?? '').toLowerCase();\n return (\n code === 'QUOTA_EXHAUSTED' ||\n errorCode === 'QUOTA_EXHAUSTED' ||\n message.includes('quota_exhausted') ||\n message.includes('quota exhausted')\n );\n });\n}\n\n/**\n * Runs the initial data query inside the ApolloProvider tree so the result\n * lands in the MFE's own Apollo cache (not the shell's).\n */\nconst InitialDataPrefetcher: FC<PropsWithChildren<{ workspaceId?: string | null }>> = ({\n workspaceId,\n children,\n}) => {\n useInitialDataLoader({ workspaceId });\n return <>{children}</>;\n};\n\n/**\n * Context value for VibeControls microfrontend\n */\ntype VibeControlsContextValue = VibeControlsRootProps;\n\nconst VibeControlsContext = createContext<VibeControlsContextValue | null>(null);\n\n/**\n * Hook to access VibeControls context\n */\nexport const useVibeControls = () => {\n const context = use(VibeControlsContext);\n if (!context) {\n throw new Error('useVibeControls must be used within VibeControlsProvider');\n }\n return context;\n};\n\n/**\n * Hook for navigation within the VibeControls microfrontend.\n * Uses the shell's navigate function if available, falling back to\n * React Router's useNavigate. Automatically prepends basePath.\n *\n * Usage:\n * const navigate = useVibeNavigate();\n * navigate('/agents/123'); // navigates to /vibecontrols/agents/123\n */\nexport const useVibeNavigate = () => {\n const { navigate: shellNavigate, basePath } = useVibeControls();\n const routerNavigate = useRouterNavigate();\n\n return useCallback(\n (path: string) => {\n const fullPath = basePath && basePath !== '/' ? `${basePath}${path}` : path;\n if (shellNavigate) {\n shellNavigate(fullPath);\n } else {\n routerNavigate(fullPath);\n }\n },\n [shellNavigate, basePath, routerNavigate]\n );\n};\n\n/**\n * Provider component for VibeControls microfrontend\n * Sets up routing, Apollo Client with auth error detection, and context.\n *\n * The Apollo Client is configured with:\n * - An onError link that detects UNAUTHENTICATED / FORBIDDEN responses and\n * invokes the `onAuthError` prop so the host shell can trigger logout.\n * - A dynamic auth link that reads the token per-request (not baked in at\n * client creation time), so token refreshes take effect immediately.\n */\nexport const VibeControlsProvider: FC<PropsWithChildren<VibeControlsRootProps>> = ({\n children,\n basePath,\n navigate,\n currentUser,\n workspaceId,\n organizationId,\n projectId,\n tenantId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n onAgentSelect,\n onSessionStart,\n onVibeActivate,\n defaultView,\n registerNavItems,\n registerFooterItems,\n onAuthError,\n}) => {\n // Per-instance QueryClient so multiple MFE mounts don't share cache\n const [queryClient] = useState(() => {\n const client = new QueryClient({\n mutationCache: new MutationCache({\n onSuccess: () => {\n void client.invalidateQueries();\n },\n }),\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n retry: 2,\n },\n },\n });\n return client;\n });\n\n // Store tokens and callbacks in refs so the Apollo Client (and its cache)\n // survives token refreshes. The setContext auth link reads from refs\n // per-request, so refreshed tokens take effect immediately without\n // destroying the InMemoryCache.\n const authTokenRef = useRef(authToken);\n const workspaceTokenRef = useRef(workspaceToken);\n const workspaceIdRef = useRef(workspaceId);\n const organizationIdRef = useRef(organizationId);\n const tenantIdRef = useRef(tenantId);\n const projectIdRef = useRef(projectId);\n const onAuthErrorRef = useRef(onAuthError);\n useEffect(() => {\n authTokenRef.current = authToken;\n }, [authToken]);\n useEffect(() => {\n workspaceTokenRef.current = workspaceToken;\n }, [workspaceToken]);\n useEffect(() => {\n workspaceIdRef.current = workspaceId;\n }, [workspaceId]);\n useEffect(() => {\n organizationIdRef.current = organizationId;\n }, [organizationId]);\n useEffect(() => {\n tenantIdRef.current = tenantId;\n }, [tenantId]);\n useEffect(() => {\n projectIdRef.current = projectId;\n }, [projectId]);\n useEffect(() => {\n onAuthErrorRef.current = onAuthError;\n }, [onAuthError]);\n\n // Build Apollo Client with error link + dynamic auth link.\n // Only recreated when apiGatewayUrl changes (different endpoint = different client).\n //\n // SCOPE NOTE (codex #128 review):\n // This client — and the `errorLink` below — is ONLY used when the microfe\n // mounts in standalone mode (no `basePath`, e.g. `bun run dev` against\n // this package directly). When integrated into an app shell (the\n // production path) `basePath` is set, the local ApolloProvider is\n // skipped further down, and mutations go through the shell's\n // `MultiGatewayProvider` Apollo client. In that integrated mode the\n // global error-dispatch path here does NOT fire — quota-handling\n // relies on the per-page `isQuotaExhaustedError(result.error)`\n // checks (e.g. TargetsPage.handleCreate) which still render the\n // local QuotaExhaustedDialog. Tracked as a follow-up: lift this\n // link into fe-libs `MultiGatewayProvider` so the global event\n // dispatches in shell mode too.\n const apolloClient = useMemo(() => {\n // Error link (Apollo v4 pattern): detect authentication failures + quota.\n //\n // Quota: we use `errorPolicy: 'all'` for mutations, so GraphQL errors land\n // on `result.error` instead of rejecting the mutation promise. Most\n // callsites only have a `catch` block, so quota errors silently disappear\n // (BOFF-2625). Dispatching a CustomEvent here lets a single provider\n // (QuotaExhaustedProvider) render the dialog for every mutation without\n // touching individual handlers. Per-page handlers that already react to\n // `isQuotaExhaustedError(result.error)` still work — both paths just open\n // the same modal state.\n const errorLink = onError(({ error, operation }) => {\n if (CombinedGraphQLErrors.is(error)) {\n // Auth first: a response can carry BOTH an UNAUTHENTICATED and a\n // QUOTA_EXHAUSTED error (token expired mid-request that also\n // triggered a quota check). If we returned after dispatching the\n // quota event, the host shell would never get the auth signal and\n // the session would stay broken behind the modal. The quota\n // dialog is moot for an expired session anyway.\n if (hasSessionAuthErrors(error.errors)) {\n console.warn('[VibeControls] Auth error detected — notifying host shell');\n onAuthErrorRef.current?.();\n return;\n }\n\n const quotaError = findQuotaExhaustedError(error.errors);\n if (quotaError) {\n const detail: QuotaExhaustedEventDetail = {\n kind: inferQuotaKindFromOperation(operation?.operationName),\n message: quotaError.message,\n operationName: operation?.operationName ?? null,\n };\n window.dispatchEvent(\n new CustomEvent<QuotaExhaustedEventDetail>(QUOTA_EXHAUSTED_EVENT, { detail })\n );\n return;\n }\n }\n\n if (error instanceof Error && isAuthNetworkError(error)) {\n console.warn('[VibeControls] Auth network error — notifying host shell');\n onAuthErrorRef.current?.();\n }\n });\n\n // Auth link: dynamically attach dual-token auth and workspace context per-request.\n // Reads from refs so token refreshes don't recreate the client.\n const authLink = setContext((_, { headers }) => {\n const resolvedAuthToken = authTokenRef.current ?? getStoredAuthToken();\n // Fall back to storage when workspaceId prop is empty (e.g. during initial render\n // before ActiveContextProvider has propagated the value through workspaceBaseProps)\n const resolvedWorkspaceId = workspaceIdRef.current || getStoredWorkspaceId();\n const resolvedProjectId = projectIdRef.current || getStoredProjectId();\n const rawWorkspaceToken = workspaceTokenRef.current ?? getStoredWorkspaceToken();\n const resolvedWorkspaceToken = isWorkspaceTokenValidForContext(rawWorkspaceToken, {\n workspaceId: resolvedWorkspaceId,\n organizationId: organizationIdRef.current,\n tenantId: tenantIdRef.current,\n projectId: resolvedProjectId,\n })\n ? rawWorkspaceToken\n : null;\n\n return {\n headers: {\n ...headers,\n ...(resolvedAuthToken ? { Authorization: `Bearer ${resolvedAuthToken}` } : {}),\n ...(resolvedWorkspaceToken\n ? { 'X-Workspace-Authorization': `Bearer ${resolvedWorkspaceToken}` }\n : {}),\n ...(resolvedWorkspaceId ? { 'x-workspace-id': resolvedWorkspaceId } : {}),\n ...(resolvedProjectId ? { 'x-project-id': resolvedProjectId } : {}),\n },\n };\n });\n\n const httpLink = new BatchHttpLink({\n uri: apiGatewayUrl ? `${apiGatewayUrl}/workspaces/graphql` : '/workspaces/graphql',\n batchMax: 10,\n batchInterval: 10, // ms — tight window so latency isn't hurt\n });\n\n let client: InstanceType<typeof ApolloClient> | null = null;\n const mutationAutoRefreshLink = createMutationAutoRefreshLink(() => client);\n\n client = new ApolloClient({\n link: from([errorLink, mutationAutoRefreshLink, authLink, httpLink]),\n cache: buildVibeControlsCache(),\n defaultOptions: {\n watchQuery: { fetchPolicy: 'cache-first', errorPolicy: 'all' },\n query: { errorPolicy: 'all' },\n mutate: { errorPolicy: 'all' },\n },\n });\n return client;\n }, [apiGatewayUrl]);\n\n const contextValue: VibeControlsContextValue = {\n basePath,\n navigate,\n currentUser,\n workspaceId,\n organizationId,\n projectId,\n tenantId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n onAgentSelect,\n onSessionStart,\n onVibeActivate,\n defaultView,\n registerNavItems,\n registerFooterItems,\n onAuthError,\n };\n\n const isPublicLinkRoute =\n typeof window !== 'undefined' &&\n /^\\/vibecontrols\\/(share|deck)\\//.test(window.location.pathname);\n\n // Core providers that wrap the children\n // PermissionProvider requires AuthProvider which is provided by the shell.\n // It fetches permissions from wspace-rbac-svc via the workspace gateway.\n // AgentEventsProvider manages WebSocket connections to agents for real-time notifications.\n //\n // QuotaExhaustedProvider wraps both public and authenticated trees because\n // public deck routes (`/vibecontrols/deck/:token`) still run mutations like\n // `ExecuteSharedVibeDeckButton` that can hit command-execution quota\n // (#128 review codex P2). The provider is cheap and route-agnostic — it\n // only listens for a window event.\n const authedTree = (\n <InitialDataPrefetcher workspaceId={workspaceId}>\n <PermissionProvider scopeId={workspaceId ?? null} gateway=\"workspace\">\n <AgentEventsProvider\n toastPosition=\"bottom-right\"\n autoDismissDelay={5000}\n maxVisibleToasts={3}\n >\n {children}\n </AgentEventsProvider>\n </PermissionProvider>\n </InitialDataPrefetcher>\n );\n\n const routedChildren = (\n <QuotaExhaustedProvider>{isPublicLinkRoute ? children : authedTree}</QuotaExhaustedProvider>\n );\n\n const coreProviders = (\n <QueryClientProvider client={queryClient}>\n {basePath ? (\n <VibeControlsContext.Provider value={contextValue}>\n {routedChildren}\n </VibeControlsContext.Provider>\n ) : (\n <ApolloProvider client={apolloClient}>\n <VibeControlsContext.Provider value={contextValue}>\n {routedChildren}\n </VibeControlsContext.Provider>\n </ApolloProvider>\n )}\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router\n // so we don't create another one. When standalone (no basePath), use MemoryRouter.\n if (basePath) {\n // Integrated mode: shell already has BrowserRouter, don't create nested router\n return coreProviders;\n }\n\n // Standalone mode: create MemoryRouter for independent operation\n return <MemoryRouter>{coreProviders}</MemoryRouter>;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VibeControlsProvider.js","names":[],"sources":["../../src/providers/VibeControlsProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, use, useMemo, useCallback, useRef, useEffect, useState } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router';\nimport { ApolloClient, ApolloLink, type FetchResult, Observable, from } from '@apollo/client';\nimport { buildVibeControlsCache } from '@/lib/apolloCache';\nimport { BatchHttpLink } from '@apollo/client/link/batch-http';\nimport { onError } from '@apollo/client/link/error';\nimport { CombinedGraphQLErrors } from '@apollo/client/errors';\nimport { setContext } from '@apollo/client/link/context';\nimport { ApolloProvider } from '@apollo/client/react';\nimport { getMainDefinition } from '@apollo/client/utilities';\nimport { MutationCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n getStoredAuthToken,\n getStoredWorkspaceToken,\n isWorkspaceTokenValidForContext,\n} from '@burdenoff/fe-libs/shared/graphql';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { isAuthNetworkError } from '@burdenoff/fe-libs/shared/utils';\nimport type { VibeControlsRootProps } from '../types';\nimport { configureSpeechEngine } from '../services/speech';\nimport { AgentEventsProvider } from './AgentEventsProvider';\nimport { QuotaExhaustedProvider } from './QuotaExhaustedProvider';\nimport { useInitialDataLoader } from '../hooks/useInitialDataLoader';\nimport {\n QUOTA_EXHAUSTED_EVENT,\n inferQuotaKindFromOperation,\n type QuotaExhaustedEventDetail,\n} from '../utils/quotaUtils';\n\ntype GraphQLErrorLike = { message?: string; extensions?: Record<string, unknown> };\n\nfunction getStoredProjectId(): string | null {\n try {\n const fromSession = sessionStorage.getItem('burdenoff-active-context-project');\n if (fromSession) return fromSession;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n const profileContext = activeProfileId\n ? localStorage.getItem(`bf-p-${activeProfileId}-context`)\n : null;\n if (!profileContext) return null;\n\n const parsed = JSON.parse(profileContext) as { projectId?: unknown };\n return typeof parsed.projectId === 'string' && parsed.projectId.length > 0\n ? parsed.projectId\n : null;\n } catch {\n return null;\n }\n}\n\nfunction hasSessionAuthErrors(errors: ReadonlyArray<GraphQLErrorLike> = []): boolean {\n return errors.some((error) => {\n const code = typeof error.extensions?.code === 'string' ? error.extensions.code : '';\n const errorCode =\n typeof error.extensions?.errorCode === 'string' ? error.extensions.errorCode : '';\n const message = (error.message ?? '').toLowerCase();\n\n return (\n code === 'UNAUTHENTICATED' ||\n errorCode === 'TOKEN_EXPIRED' ||\n message.includes('token_expired') ||\n message.includes('token expired') ||\n message.includes('session expired') ||\n message.includes('unauthorized') ||\n message.includes('unauthenticated') ||\n message.includes('authentication required') ||\n message.includes('workspace context missing')\n );\n });\n}\n\nfunction isMutationOperation(query: Parameters<typeof getMainDefinition>[0]): boolean {\n const definition = getMainDefinition(query);\n return definition.kind === 'OperationDefinition' && definition.operation === 'mutation';\n}\n\nfunction hasGraphQLResultErrors(result: FetchResult<Record<string, unknown>>): boolean {\n const errors = result.errors;\n return Array.isArray(errors) && errors.length > 0;\n}\n\nfunction createMutationAutoRefreshLink(\n getClient: () => InstanceType<typeof ApolloClient> | null\n): ApolloLink {\n return new ApolloLink((operation, forward) => {\n const shouldRefreshAfterMutation =\n isMutationOperation(operation.query) &&\n operation.getContext().autoRefreshAfterMutation !== false &&\n operation.getContext().skipMutationAutoRefresh !== true;\n\n return new Observable<FetchResult<Record<string, unknown>>>((observer) => {\n const subscription = forward(operation).subscribe({\n next: (result) => {\n observer.next(result as FetchResult<Record<string, unknown>>);\n if (!shouldRefreshAfterMutation || hasGraphQLResultErrors(result)) return;\n\n void getClient()?.refetchQueries({ include: 'active' });\n },\n error: (error: unknown) => {\n observer.error(error);\n },\n complete: () => {\n observer.complete();\n },\n });\n\n return () => {\n subscription.unsubscribe();\n };\n });\n });\n}\n\nfunction findQuotaExhaustedError(\n errors: ReadonlyArray<GraphQLErrorLike> = []\n): GraphQLErrorLike | undefined {\n return errors.find((error) => {\n const code = typeof error.extensions?.code === 'string' ? error.extensions.code : '';\n const errorCode =\n typeof error.extensions?.errorCode === 'string' ? error.extensions.errorCode : '';\n // Match both the constant form and the human-readable phrase, case\n // insensitively — mirrors what `isQuotaExhaustedError` accepts at the\n // per-page level so the link doesn't miss any quota error the rest\n // of the code base already recognises.\n const message = (error.message ?? '').toLowerCase();\n return (\n code === 'QUOTA_EXHAUSTED' ||\n errorCode === 'QUOTA_EXHAUSTED' ||\n message.includes('quota_exhausted') ||\n message.includes('quota exhausted')\n );\n });\n}\n\n/**\n * Runs the initial data query inside the ApolloProvider tree so the result\n * lands in the MFE's own Apollo cache (not the shell's).\n */\nconst InitialDataPrefetcher: FC<PropsWithChildren<{ workspaceId?: string | null }>> = ({\n workspaceId,\n children,\n}) => {\n useInitialDataLoader({ workspaceId });\n return <>{children}</>;\n};\n\n/**\n * Context value for VibeControls microfrontend\n */\ntype VibeControlsContextValue = VibeControlsRootProps;\n\nconst VibeControlsContext = createContext<VibeControlsContextValue | null>(null);\n\n/**\n * Hook to access VibeControls context\n */\nexport const useVibeControls = () => {\n const context = use(VibeControlsContext);\n if (!context) {\n throw new Error('useVibeControls must be used within VibeControlsProvider');\n }\n return context;\n};\n\n/**\n * Hook for navigation within the VibeControls microfrontend.\n * Uses the shell's navigate function if available, falling back to\n * React Router's useNavigate. Automatically prepends basePath.\n *\n * Usage:\n * const navigate = useVibeNavigate();\n * navigate('/agents/123'); // navigates to /vibecontrols/agents/123\n */\nexport const useVibeNavigate = () => {\n const { navigate: shellNavigate, basePath } = useVibeControls();\n const routerNavigate = useRouterNavigate();\n\n return useCallback(\n (path: string) => {\n const fullPath = basePath && basePath !== '/' ? `${basePath}${path}` : path;\n if (shellNavigate) {\n shellNavigate(fullPath);\n } else {\n routerNavigate(fullPath);\n }\n },\n [shellNavigate, basePath, routerNavigate]\n );\n};\n\n/**\n * Provider component for VibeControls microfrontend\n * Sets up routing, Apollo Client with auth error detection, and context.\n *\n * The Apollo Client is configured with:\n * - An onError link that detects UNAUTHENTICATED / FORBIDDEN responses and\n * invokes the `onAuthError` prop so the host shell can trigger logout.\n * - A dynamic auth link that reads the token per-request (not baked in at\n * client creation time), so token refreshes take effect immediately.\n */\nexport const VibeControlsProvider: FC<PropsWithChildren<VibeControlsRootProps>> = ({\n children,\n basePath,\n navigate,\n currentUser,\n workspaceId,\n organizationId,\n projectId,\n tenantId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n onAgentSelect,\n onSessionStart,\n onVibeActivate,\n defaultView,\n registerNavItems,\n registerFooterItems,\n onAuthError,\n ttsEndpoint,\n}) => {\n // Per-instance QueryClient so multiple MFE mounts don't share cache\n const [queryClient] = useState(() => {\n const client = new QueryClient({\n mutationCache: new MutationCache({\n onSuccess: () => {\n void client.invalidateQueries();\n },\n }),\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n retry: 2,\n },\n },\n });\n return client;\n });\n\n // Store tokens and callbacks in refs so the Apollo Client (and its cache)\n // survives token refreshes. The setContext auth link reads from refs\n // per-request, so refreshed tokens take effect immediately without\n // destroying the InMemoryCache.\n const authTokenRef = useRef(authToken);\n const workspaceTokenRef = useRef(workspaceToken);\n const workspaceIdRef = useRef(workspaceId);\n const organizationIdRef = useRef(organizationId);\n const tenantIdRef = useRef(tenantId);\n const projectIdRef = useRef(projectId);\n const onAuthErrorRef = useRef(onAuthError);\n useEffect(() => {\n authTokenRef.current = authToken;\n }, [authToken]);\n\n /**\n * Point the assistant's speech stack at the hosted TTS proxy.\n *\n * ★★ Without this the assistant speaks in DEVICE voices only. `getSpeechEngine`\n * falls back to `browserSpeechEngine` whenever no endpoint is configured, so\n * the feature degrades silently — you get a robotic system voice and no voice\n * picker, because the selector hides itself when fewer than two voices are\n * available for the locale. Nothing errors, nothing logs.\n *\n * ★ Wired HERE rather than exported for the host to call. Nothing outside this\n * module graph would import `configureSpeechEngine`, and fe-libs is bundled\n * with rolldown — an unreferenced export is tree-shaken out of `dist`, so the\n * function would not exist at runtime to be called. HealthyBowl learned this\n * the same way.\n *\n * ★★ The token is read through a REF, not captured. `configureSpeechEngine` is\n * deliberately idempotent — resetting the memoised engine every render would\n * cancel playback mid-reply — so it keeps the FIRST `getHeaders` it is given.\n * A captured token would work until it refreshed and then fail mid-sentence,\n * which is the hardest place to notice. The proxy bills per character, so an\n * endpoint reachable without headers is a metered API open to anyone who reads\n * the bundle.\n */\n const getTtsHeaders = useCallback(\n (): Record<string, string> =>\n authTokenRef.current ? { authorization: `Bearer ${authTokenRef.current}` } : {},\n []\n );\n configureSpeechEngine({ ttsEndpoint, getHeaders: getTtsHeaders });\n useEffect(() => {\n workspaceTokenRef.current = workspaceToken;\n }, [workspaceToken]);\n useEffect(() => {\n workspaceIdRef.current = workspaceId;\n }, [workspaceId]);\n useEffect(() => {\n organizationIdRef.current = organizationId;\n }, [organizationId]);\n useEffect(() => {\n tenantIdRef.current = tenantId;\n }, [tenantId]);\n useEffect(() => {\n projectIdRef.current = projectId;\n }, [projectId]);\n useEffect(() => {\n onAuthErrorRef.current = onAuthError;\n }, [onAuthError]);\n\n // Build Apollo Client with error link + dynamic auth link.\n // Only recreated when apiGatewayUrl changes (different endpoint = different client).\n //\n // SCOPE NOTE (codex #128 review):\n // This client — and the `errorLink` below — is ONLY used when the microfe\n // mounts in standalone mode (no `basePath`, e.g. `bun run dev` against\n // this package directly). When integrated into an app shell (the\n // production path) `basePath` is set, the local ApolloProvider is\n // skipped further down, and mutations go through the shell's\n // `MultiGatewayProvider` Apollo client. In that integrated mode the\n // global error-dispatch path here does NOT fire — quota-handling\n // relies on the per-page `isQuotaExhaustedError(result.error)`\n // checks (e.g. TargetsPage.handleCreate) which still render the\n // local QuotaExhaustedDialog. Tracked as a follow-up: lift this\n // link into fe-libs `MultiGatewayProvider` so the global event\n // dispatches in shell mode too.\n const apolloClient = useMemo(() => {\n // Error link (Apollo v4 pattern): detect authentication failures + quota.\n //\n // Quota: we use `errorPolicy: 'all'` for mutations, so GraphQL errors land\n // on `result.error` instead of rejecting the mutation promise. Most\n // callsites only have a `catch` block, so quota errors silently disappear\n // (BOFF-2625). Dispatching a CustomEvent here lets a single provider\n // (QuotaExhaustedProvider) render the dialog for every mutation without\n // touching individual handlers. Per-page handlers that already react to\n // `isQuotaExhaustedError(result.error)` still work — both paths just open\n // the same modal state.\n const errorLink = onError(({ error, operation }) => {\n if (CombinedGraphQLErrors.is(error)) {\n // Auth first: a response can carry BOTH an UNAUTHENTICATED and a\n // QUOTA_EXHAUSTED error (token expired mid-request that also\n // triggered a quota check). If we returned after dispatching the\n // quota event, the host shell would never get the auth signal and\n // the session would stay broken behind the modal. The quota\n // dialog is moot for an expired session anyway.\n if (hasSessionAuthErrors(error.errors)) {\n console.warn('[VibeControls] Auth error detected — notifying host shell');\n onAuthErrorRef.current?.();\n return;\n }\n\n const quotaError = findQuotaExhaustedError(error.errors);\n if (quotaError) {\n const detail: QuotaExhaustedEventDetail = {\n kind: inferQuotaKindFromOperation(operation?.operationName),\n message: quotaError.message,\n operationName: operation?.operationName ?? null,\n };\n window.dispatchEvent(\n new CustomEvent<QuotaExhaustedEventDetail>(QUOTA_EXHAUSTED_EVENT, { detail })\n );\n return;\n }\n }\n\n if (error instanceof Error && isAuthNetworkError(error)) {\n console.warn('[VibeControls] Auth network error — notifying host shell');\n onAuthErrorRef.current?.();\n }\n });\n\n // Auth link: dynamically attach dual-token auth and workspace context per-request.\n // Reads from refs so token refreshes don't recreate the client.\n const authLink = setContext((_, { headers }) => {\n const resolvedAuthToken = authTokenRef.current ?? getStoredAuthToken();\n // Fall back to storage when workspaceId prop is empty (e.g. during initial render\n // before ActiveContextProvider has propagated the value through workspaceBaseProps)\n const resolvedWorkspaceId = workspaceIdRef.current || getStoredWorkspaceId();\n const resolvedProjectId = projectIdRef.current || getStoredProjectId();\n const rawWorkspaceToken = workspaceTokenRef.current ?? getStoredWorkspaceToken();\n const resolvedWorkspaceToken = isWorkspaceTokenValidForContext(rawWorkspaceToken, {\n workspaceId: resolvedWorkspaceId,\n organizationId: organizationIdRef.current,\n tenantId: tenantIdRef.current,\n projectId: resolvedProjectId,\n })\n ? rawWorkspaceToken\n : null;\n\n return {\n headers: {\n ...headers,\n ...(resolvedAuthToken ? { Authorization: `Bearer ${resolvedAuthToken}` } : {}),\n ...(resolvedWorkspaceToken\n ? { 'X-Workspace-Authorization': `Bearer ${resolvedWorkspaceToken}` }\n : {}),\n ...(resolvedWorkspaceId ? { 'x-workspace-id': resolvedWorkspaceId } : {}),\n ...(resolvedProjectId ? { 'x-project-id': resolvedProjectId } : {}),\n },\n };\n });\n\n const httpLink = new BatchHttpLink({\n uri: apiGatewayUrl ? `${apiGatewayUrl}/workspaces/graphql` : '/workspaces/graphql',\n batchMax: 10,\n batchInterval: 10, // ms — tight window so latency isn't hurt\n });\n\n let client: InstanceType<typeof ApolloClient> | null = null;\n const mutationAutoRefreshLink = createMutationAutoRefreshLink(() => client);\n\n client = new ApolloClient({\n link: from([errorLink, mutationAutoRefreshLink, authLink, httpLink]),\n cache: buildVibeControlsCache(),\n defaultOptions: {\n watchQuery: { fetchPolicy: 'cache-first', errorPolicy: 'all' },\n query: { errorPolicy: 'all' },\n mutate: { errorPolicy: 'all' },\n },\n });\n return client;\n }, [apiGatewayUrl]);\n\n const contextValue: VibeControlsContextValue = {\n basePath,\n navigate,\n currentUser,\n workspaceId,\n organizationId,\n projectId,\n tenantId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n onAgentSelect,\n onSessionStart,\n onVibeActivate,\n defaultView,\n registerNavItems,\n registerFooterItems,\n onAuthError,\n };\n\n const isPublicLinkRoute =\n typeof window !== 'undefined' &&\n /^\\/vibecontrols\\/(share|deck)\\//.test(window.location.pathname);\n\n // Core providers that wrap the children\n // PermissionProvider requires AuthProvider which is provided by the shell.\n // It fetches permissions from wspace-rbac-svc via the workspace gateway.\n // AgentEventsProvider manages WebSocket connections to agents for real-time notifications.\n //\n // QuotaExhaustedProvider wraps both public and authenticated trees because\n // public deck routes (`/vibecontrols/deck/:token`) still run mutations like\n // `ExecuteSharedVibeDeckButton` that can hit command-execution quota\n // (#128 review codex P2). The provider is cheap and route-agnostic — it\n // only listens for a window event.\n const authedTree = (\n <InitialDataPrefetcher workspaceId={workspaceId}>\n <PermissionProvider scopeId={workspaceId ?? null} gateway=\"workspace\">\n <AgentEventsProvider\n toastPosition=\"bottom-right\"\n autoDismissDelay={5000}\n maxVisibleToasts={3}\n >\n {children}\n </AgentEventsProvider>\n </PermissionProvider>\n </InitialDataPrefetcher>\n );\n\n const routedChildren = (\n <QuotaExhaustedProvider>{isPublicLinkRoute ? children : authedTree}</QuotaExhaustedProvider>\n );\n\n const coreProviders = (\n <QueryClientProvider client={queryClient}>\n {basePath ? (\n <VibeControlsContext.Provider value={contextValue}>\n {routedChildren}\n </VibeControlsContext.Provider>\n ) : (\n <ApolloProvider client={apolloClient}>\n <VibeControlsContext.Provider value={contextValue}>\n {routedChildren}\n </VibeControlsContext.Provider>\n </ApolloProvider>\n )}\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router\n // so we don't create another one. When standalone (no basePath), use MemoryRouter.\n if (basePath) {\n // Integrated mode: shell already has BrowserRouter, don't create nested router\n return coreProviders;\n }\n\n // Standalone mode: create MemoryRouter for independent operation\n return <MemoryRouter>{coreProviders}</MemoryRouter>;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistantApi.js","names":[],"sources":["../../src/services/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API — thin adapter over @burdenoff/fe-libs/shared/assistant.\n *\n * The legacy slide-in panel (AssistantChatArea / AssistantMessageItem /\n * useAssistantHeartbeat) calls these with the historical (apollo-first)\n * signatures. The real implementation now lives once in fe-libs; this file only\n * maps the old signatures onto it, retiring ~775 lines of duplicated logic. The\n * `apollo` argument is vestigial (the shared layer uses graphqlFetch) and is\n * ignored; the product id + prod-runtime + TTL the shared functions need are\n * supplied here.\n */\n\nimport {\n findExistingSandbox as sharedFindExisting,\n createAssistantSandbox as sharedCreate,\n waitForSandboxRunning as sharedWaitRunning,\n waitForAssistantServiceReady as sharedWaitService,\n createAssistantSession as sharedCreateSession,\n getAssistantSession as sharedGetSession,\n sendAssistantPromptAsync as sharedSendAsync,\n getAssistantMessages as sharedGetMessages,\n extendAssistantSandboxTTL as sharedExtendTtl,\n sendHeartbeat as sharedHeartbeat,\n getAssistantArtifacts as sharedGetArtifacts,\n downloadAssistantArtifact as sharedDownloadArtifact,\n checkAssistantAiCredits as sharedCheckCredits,\n reportAssistantAiTokenUsage as sharedReportUsage,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport type {\n AssistantRawMessage as SharedRawMessage,\n AssistantSandboxAuthContext as SharedAuthContext,\n AssistantSessionInfo as SharedSessionInfo,\n DownloadedAssistantArtifact,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport type { AssistantArtifact as LocalAssistantArtifact, AssistantMode } from '@/types/assistant';\nimport type { PageContext } from '@/utils/pageContext';\n\n// Re-export the shared types under the names the panel already imports.\nexport type AssistantRawMessage = SharedRawMessage;\nexport type AssistantSandboxAuthContext = SharedAuthContext;\nexport type AssistantSessionInfo = SharedSessionInfo;\n\n// Vestigial Apollo arg — accepted for signature compatibility, ignored.\ntype ApolloClientLike = unknown;\n\nexport const PRODUCT = 'vibecontrols';\n// Matches the original panel behavior: create at 600s, kept warm while open by\n// useAssistantHeartbeat's +600s extend. (The floating widget keeps its own\n// longer-TTL keep-warm via the shared transport.)\nconst DEFAULT_TTL_SECONDS = 600;\n\n// Build-time env parity with the original `assistantApi.ts` image-tag selection\n// (VITE_APP_ENV === 'prod' → prod image tag). Drives getImageTagForMode.\nfunction isProdRuntime(): boolean {\n return import.meta.env.VITE_APP_ENV === 'prod';\n}\n\nconst ctx = (a?: SharedAuthContext): SharedAuthContext => a ?? {};\n\nexport function findExistingSandbox(\n _apollo: ApolloClientLike,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: SharedAuthContext\n): Promise<string | null> {\n return sharedFindExisting(workspaceId, mode, isProdRuntime(), ctx(authContext));\n}\n\nexport function createAssistantSandbox(\n _apollo: ApolloClientLike,\n mode: AssistantMode,\n workspaceId: string,\n authContext?: SharedAuthContext\n): Promise<string> {\n return sharedCreate(\n PRODUCT,\n mode,\n workspaceId,\n isProdRuntime(),\n DEFAULT_TTL_SECONDS,\n ctx(authContext)\n );\n}\n\nexport function waitForSandboxReady(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n authContext?: SharedAuthContext,\n maxWaitMs = 300_000,\n _onProgress?: (message: string) => void\n): Promise<void> {\n return sharedWaitRunning(sandboxId, workspaceId, ctx(authContext), maxWaitMs);\n}\n\nexport function waitForAssistantServiceReady(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n authContext?: SharedAuthContext,\n maxWaitMs = 90_000,\n _onProgress?: (message: string) => void\n): Promise<void> {\n return sharedWaitService(sandboxId, workspaceId, ctx(authContext), maxWaitMs);\n}\n\nexport function createAssistantSession(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: SharedAuthContext\n): Promise<{ sessionId: string }> {\n return sharedCreateSession(sandboxId, workspaceId, mode, ctx(authContext));\n}\n\nexport function getAssistantSession(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<SharedSessionInfo> {\n return sharedGetSession(sandboxId, workspaceId, sessionId, ctx(authContext));\n}\n\nexport function sendAssistantPromptAsync(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: SharedAuthContext\n): Promise<{ messageID?: string }> {\n return sharedSendAsync(\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n mode,\n pageContext as Record<string, unknown> | undefined,\n ctx(authContext)\n );\n}\n\nexport function getAssistantMessages(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext,\n limit = 50\n): Promise<SharedRawMessage[]> {\n return sharedGetMessages(sandboxId, workspaceId, sessionId, ctx(authContext), { limit });\n}\n\nexport function checkAssistantAiCredits(\n _apollo: ApolloClientLike,\n workspaceId: string,\n authContext?: SharedAuthContext\n): Promise<boolean> {\n return sharedCheckCredits(workspaceId, ctx(authContext));\n}\n\nexport function reportAssistantAiTokenUsage(\n _apollo: ApolloClientLike,\n workspaceId: string,\n sessionId: string,\n messageId: string,\n model: string | undefined,\n tokens: { input: number; output: number },\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedReportUsage(workspaceId, sessionId, messageId, model, tokens, ctx(authContext));\n}\n\n/**\n * Object-shaped adapter for the shared product seam.\n *\n * ★ The seam's `reportTokenUsage` takes ONE object; VibeControls' own reporter\n * is positional and leads with an unused Apollo client. Rather than change a\n * signature the existing chat area still calls, adapt here — the seam gets the\n * shape it declares and nothing else moves.\n *\n * ★★ A missing `messageId` or `model` is logged, not swallowed. This meter\n * exists because assistant spend was invisible; a silently skipped charge would\n * recreate exactly that blind spot, one turn at a time. Metering must never\n * break a turn, so this resolves either way.\n */\nexport async function reportAssistantTokenUsage(usage: {\n workspaceId: string;\n sessionId: string;\n messageId?: string;\n model?: string;\n tokens: { input: number; output: number };\n}): Promise<void> {\n const { workspaceId, sessionId, messageId, model, tokens } = usage;\n if (!messageId || !model) {\n console.warn('[assistant] skipping AI-credit charge — turn is missing messageId or model', {\n sessionId,\n hasMessageId: Boolean(messageId),\n hasModel: Boolean(model),\n });\n return;\n }\n await reportAssistantAiTokenUsage(\n null as never, // the positional Apollo client is unused (`_apollo`)\n workspaceId,\n sessionId,\n messageId,\n model,\n tokens\n );\n}\n\nexport function getAssistantArtifacts(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<LocalAssistantArtifact[]> {\n // The shared surface widens `type` to `string`; at runtime the agent only ever\n // returns 'file' | 'directory', so the array is structurally the local type.\n return sharedGetArtifacts(sandboxId, workspaceId, sessionId, ctx(authContext)).then(\n (artifacts) => artifacts as unknown as LocalAssistantArtifact[]\n );\n}\n\nexport function downloadAssistantArtifact(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n artifactPath: string,\n authContext?: SharedAuthContext\n): Promise<DownloadedAssistantArtifact> {\n return sharedDownloadArtifact(sandboxId, workspaceId, sessionId, artifactPath, ctx(authContext));\n}\n\nexport function sendHeartbeat(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedHeartbeat(sandboxId, workspaceId, sessionId, ctx(authContext));\n}\n\nexport function extendAssistantSandboxTTL(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedExtendTtl(sandboxId, workspaceId, additionalSeconds, ctx(authContext));\n}\n"],"mappings":";;AA6CA,IAAa,IAAU,gBAYjB,KAAO,MAA6C,KAAK,EAAE;
|
|
1
|
+
{"version":3,"file":"assistantApi.js","names":[],"sources":["../../src/services/assistantApi.ts"],"sourcesContent":["/**\n * AI Assistant API — thin adapter over @burdenoff/fe-libs/shared/assistant.\n *\n * The legacy slide-in panel (AssistantChatArea / AssistantMessageItem /\n * useAssistantHeartbeat) calls these with the historical (apollo-first)\n * signatures. The real implementation now lives once in fe-libs; this file only\n * maps the old signatures onto it, retiring ~775 lines of duplicated logic. The\n * `apollo` argument is vestigial (the shared layer uses graphqlFetch) and is\n * ignored; the product id + prod-runtime + TTL the shared functions need are\n * supplied here.\n */\n\nimport {\n findExistingSandbox as sharedFindExisting,\n createAssistantSandbox as sharedCreate,\n waitForSandboxRunning as sharedWaitRunning,\n waitForAssistantServiceReady as sharedWaitService,\n createAssistantSession as sharedCreateSession,\n getAssistantSession as sharedGetSession,\n sendAssistantPromptAsync as sharedSendAsync,\n getAssistantMessages as sharedGetMessages,\n extendAssistantSandboxTTL as sharedExtendTtl,\n sendHeartbeat as sharedHeartbeat,\n getAssistantArtifacts as sharedGetArtifacts,\n downloadAssistantArtifact as sharedDownloadArtifact,\n checkAssistantAiCredits as sharedCheckCredits,\n reportAssistantAiTokenUsage as sharedReportUsage,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport type {\n AssistantRawMessage as SharedRawMessage,\n AssistantSandboxAuthContext as SharedAuthContext,\n AssistantSessionInfo as SharedSessionInfo,\n DownloadedAssistantArtifact,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport type { AssistantArtifact as LocalAssistantArtifact, AssistantMode } from '@/types/assistant';\nimport type { PageContext } from '@/utils/pageContext';\n\n// Re-export the shared types under the names the panel already imports.\nexport type AssistantRawMessage = SharedRawMessage;\nexport type AssistantSandboxAuthContext = SharedAuthContext;\nexport type AssistantSessionInfo = SharedSessionInfo;\n\n// Vestigial Apollo arg — accepted for signature compatibility, ignored.\ntype ApolloClientLike = unknown;\n\nexport const PRODUCT = 'vibecontrols';\n// Matches the original panel behavior: create at 600s, kept warm while open by\n// useAssistantHeartbeat's +600s extend. (The floating widget keeps its own\n// longer-TTL keep-warm via the shared transport.)\nconst DEFAULT_TTL_SECONDS = 600;\n\n// Build-time env parity with the original `assistantApi.ts` image-tag selection\n// (VITE_APP_ENV === 'prod' → prod image tag). Drives getImageTagForMode.\nfunction isProdRuntime(): boolean {\n return import.meta.env.VITE_APP_ENV === 'prod';\n}\n\nconst ctx = (a?: SharedAuthContext): SharedAuthContext => a ?? {};\n\nexport function findExistingSandbox(\n _apollo: ApolloClientLike,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: SharedAuthContext\n): Promise<string | null> {\n // ★ fe-libs added `product` as the SECOND argument when sandbox reuse became\n // product-isolated — without it this shim would ask for any product's sandbox.\n // `createAssistantSandbox` below already passed PRODUCT; this one had not been\n // updated, and the stale fe-libs pin hid the mismatch.\n return sharedFindExisting(workspaceId, PRODUCT, mode, isProdRuntime(), ctx(authContext));\n}\n\nexport function createAssistantSandbox(\n _apollo: ApolloClientLike,\n mode: AssistantMode,\n workspaceId: string,\n authContext?: SharedAuthContext\n): Promise<string> {\n return sharedCreate(\n PRODUCT,\n mode,\n workspaceId,\n isProdRuntime(),\n DEFAULT_TTL_SECONDS,\n ctx(authContext)\n );\n}\n\nexport function waitForSandboxReady(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n authContext?: SharedAuthContext,\n maxWaitMs = 300_000,\n _onProgress?: (message: string) => void\n): Promise<void> {\n return sharedWaitRunning(sandboxId, workspaceId, ctx(authContext), maxWaitMs);\n}\n\nexport function waitForAssistantServiceReady(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n authContext?: SharedAuthContext,\n maxWaitMs = 90_000,\n _onProgress?: (message: string) => void\n): Promise<void> {\n return sharedWaitService(sandboxId, workspaceId, ctx(authContext), maxWaitMs);\n}\n\nexport function createAssistantSession(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n mode: AssistantMode,\n authContext?: SharedAuthContext\n): Promise<{ sessionId: string }> {\n return sharedCreateSession(sandboxId, workspaceId, mode, ctx(authContext));\n}\n\nexport function getAssistantSession(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<SharedSessionInfo> {\n return sharedGetSession(sandboxId, workspaceId, sessionId, ctx(authContext));\n}\n\nexport function sendAssistantPromptAsync(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n prompt: string,\n mode: AssistantMode,\n pageContext?: PageContext,\n authContext?: SharedAuthContext\n): Promise<{ messageID?: string }> {\n return sharedSendAsync(\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n mode,\n pageContext as Record<string, unknown> | undefined,\n ctx(authContext)\n );\n}\n\nexport function getAssistantMessages(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext,\n limit = 50\n): Promise<SharedRawMessage[]> {\n return sharedGetMessages(sandboxId, workspaceId, sessionId, ctx(authContext), { limit });\n}\n\nexport function checkAssistantAiCredits(\n _apollo: ApolloClientLike,\n workspaceId: string,\n authContext?: SharedAuthContext\n): Promise<boolean> {\n return sharedCheckCredits(workspaceId, ctx(authContext));\n}\n\nexport function reportAssistantAiTokenUsage(\n _apollo: ApolloClientLike,\n workspaceId: string,\n sessionId: string,\n messageId: string,\n model: string | undefined,\n tokens: { input: number; output: number },\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedReportUsage(workspaceId, sessionId, messageId, model, tokens, ctx(authContext));\n}\n\n/**\n * Object-shaped adapter for the shared product seam.\n *\n * ★ The seam's `reportTokenUsage` takes ONE object; VibeControls' own reporter\n * is positional and leads with an unused Apollo client. Rather than change a\n * signature the existing chat area still calls, adapt here — the seam gets the\n * shape it declares and nothing else moves.\n *\n * ★★ A missing `messageId` or `model` is logged, not swallowed. This meter\n * exists because assistant spend was invisible; a silently skipped charge would\n * recreate exactly that blind spot, one turn at a time. Metering must never\n * break a turn, so this resolves either way.\n */\nexport async function reportAssistantTokenUsage(usage: {\n workspaceId: string;\n sessionId: string;\n messageId?: string;\n model?: string;\n tokens: { input: number; output: number };\n}): Promise<void> {\n const { workspaceId, sessionId, messageId, model, tokens } = usage;\n if (!messageId || !model) {\n console.warn('[assistant] skipping AI-credit charge — turn is missing messageId or model', {\n sessionId,\n hasMessageId: Boolean(messageId),\n hasModel: Boolean(model),\n });\n return;\n }\n await reportAssistantAiTokenUsage(\n null as never, // the positional Apollo client is unused (`_apollo`)\n workspaceId,\n sessionId,\n messageId,\n model,\n tokens\n );\n}\n\nexport function getAssistantArtifacts(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<LocalAssistantArtifact[]> {\n // The shared surface widens `type` to `string`; at runtime the agent only ever\n // returns 'file' | 'directory', so the array is structurally the local type.\n return sharedGetArtifacts(sandboxId, workspaceId, sessionId, ctx(authContext)).then(\n (artifacts) => artifacts as unknown as LocalAssistantArtifact[]\n );\n}\n\nexport function downloadAssistantArtifact(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n artifactPath: string,\n authContext?: SharedAuthContext\n): Promise<DownloadedAssistantArtifact> {\n return sharedDownloadArtifact(sandboxId, workspaceId, sessionId, artifactPath, ctx(authContext));\n}\n\nexport function sendHeartbeat(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n sessionId: string,\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedHeartbeat(sandboxId, workspaceId, sessionId, ctx(authContext));\n}\n\nexport function extendAssistantSandboxTTL(\n _apollo: ApolloClientLike,\n sandboxId: string,\n workspaceId: string,\n additionalSeconds = 600,\n authContext?: SharedAuthContext\n): Promise<void> {\n return sharedExtendTtl(sandboxId, workspaceId, additionalSeconds, ctx(authContext));\n}\n"],"mappings":";;AA6CA,IAAa,IAAU,gBAYjB,KAAO,MAA6C,KAAK,EAAE;AAiHjE,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,GACe;AACf,QAAO,EAAkB,GAAa,GAAW,GAAW,GAAO,GAAQ,EAAI,EAAY,CAAC;;AAgB9F,eAAsB,EAA0B,GAM9B;CAChB,IAAM,EAAE,gBAAa,cAAW,cAAW,UAAO,cAAW;AAC7D,KAAI,CAAC,KAAa,CAAC,GAAO;AACxB,UAAQ,KAAK,8EAA8E;GACzF;GACA,cAAc,EAAQ;GACtB,UAAU,EAAQ;GACnB,CAAC;AACF;;AAEF,OAAM,EACJ,MACA,GACA,GACA,GACA,GACA,EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-vibecontrols",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.910.1",
|
|
4
4
|
"description": "VibeControls microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"license": "PROPRIETARY",
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@apollo/client": "4.1.9",
|
|
65
|
-
"@burdenoff/fe-libs": "2026.
|
|
65
|
+
"@burdenoff/fe-libs": "2026.910.4",
|
|
66
66
|
"@tanstack/react-query": "^5.90.16",
|
|
67
67
|
"@xyflow/react": "^12.10.2",
|
|
68
68
|
"clsx": "^2.1.1",
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
-
import { useMemo as t, useState as n } from "react";
|
|
3
|
-
import { Check as r, Copy as i } from "lucide-react";
|
|
4
|
-
import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
|
-
//#region src/components/assistant/AssistantMarkdownRenderer.tsx
|
|
6
|
-
function c({ language: t, code: c }) {
|
|
7
|
-
let l = e(), [u, d] = n(!1);
|
|
8
|
-
return /* @__PURE__ */ s("div", {
|
|
9
|
-
className: "group relative my-2 rounded-md bg-bg-sunken border border-border-default overflow-hidden",
|
|
10
|
-
children: [/* @__PURE__ */ s("div", {
|
|
11
|
-
className: "flex items-center justify-between px-3 py-1.5 bg-bg-elevated border-b border-border-default",
|
|
12
|
-
children: [/* @__PURE__ */ o("span", {
|
|
13
|
-
className: "text-[10px] font-mono text-text-muted uppercase tracking-wider",
|
|
14
|
-
children: t || "code"
|
|
15
|
-
}), /* @__PURE__ */ o("button", {
|
|
16
|
-
type: "button",
|
|
17
|
-
onClick: () => {
|
|
18
|
-
navigator.clipboard.writeText(c), d(!0), setTimeout(() => d(!1), 2e3);
|
|
19
|
-
},
|
|
20
|
-
className: "flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded transition-colors",
|
|
21
|
-
"aria-label": l("vibecontrols.assistant.markdown.copyCode", "Copy code"),
|
|
22
|
-
children: u ? /* @__PURE__ */ s(a, { children: [/* @__PURE__ */ o(r, { className: "size-3 text-status-success-text" }), /* @__PURE__ */ o("span", { children: l("vibecontrols.assistant.markdown.copied", "Copied") })] }) : /* @__PURE__ */ s(a, { children: [/* @__PURE__ */ o(i, { className: "size-3" }), /* @__PURE__ */ o("span", { children: l("vibecontrols.assistant.markdown.copy", "Copy") })] })
|
|
23
|
-
})]
|
|
24
|
-
}), /* @__PURE__ */ o("pre", {
|
|
25
|
-
className: "p-3 overflow-x-auto text-xs leading-relaxed",
|
|
26
|
-
children: /* @__PURE__ */ o("code", {
|
|
27
|
-
className: "font-mono text-text-primary",
|
|
28
|
-
children: c
|
|
29
|
-
})
|
|
30
|
-
})]
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
function l(e) {
|
|
34
|
-
let t = [], n = 0, r = e.split(/(```[\s\S]*?```)/g);
|
|
35
|
-
for (let e of r) {
|
|
36
|
-
let r = e.match(/^```(\w*)\n?([\s\S]*?)```$/);
|
|
37
|
-
if (r) t.push({
|
|
38
|
-
type: "code",
|
|
39
|
-
language: r[1],
|
|
40
|
-
content: r[2].trimEnd(),
|
|
41
|
-
key: n++
|
|
42
|
-
});
|
|
43
|
-
else if (e.trim()) {
|
|
44
|
-
let r = e.split("\n"), i = 0, a = "";
|
|
45
|
-
for (; i < r.length;) if (r[i].includes("|") && i + 1 < r.length && /^\|?[\s-:|]+\|/.test(r[i + 1])) {
|
|
46
|
-
a.trim() && (t.push({
|
|
47
|
-
type: "text",
|
|
48
|
-
content: a.trim(),
|
|
49
|
-
key: n++
|
|
50
|
-
}), a = "");
|
|
51
|
-
let e = "";
|
|
52
|
-
for (; i < r.length && r[i].includes("|");) e += r[i] + "\n", i++;
|
|
53
|
-
t.push({
|
|
54
|
-
type: "table",
|
|
55
|
-
content: e.trim(),
|
|
56
|
-
key: n++
|
|
57
|
-
});
|
|
58
|
-
} else a += r[i] + "\n", i++;
|
|
59
|
-
a.trim() && t.push({
|
|
60
|
-
type: "text",
|
|
61
|
-
content: a.trim(),
|
|
62
|
-
key: n++
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return t;
|
|
67
|
-
}
|
|
68
|
-
function u(e) {
|
|
69
|
-
let t = [], n = e, r = 0;
|
|
70
|
-
for (; n.length > 0;) {
|
|
71
|
-
let e = n.match(/\*\*(.+?)\*\*/), i = n.match(/`([^`]+)`/), a = n.match(/\[([^\]]+)\]\(([^)]+)\)/), s = n.match(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/), c = [
|
|
72
|
-
e && {
|
|
73
|
-
type: "bold",
|
|
74
|
-
match: e,
|
|
75
|
-
index: e.index
|
|
76
|
-
},
|
|
77
|
-
i && {
|
|
78
|
-
type: "code",
|
|
79
|
-
match: i,
|
|
80
|
-
index: i.index
|
|
81
|
-
},
|
|
82
|
-
a && {
|
|
83
|
-
type: "link",
|
|
84
|
-
match: a,
|
|
85
|
-
index: a.index
|
|
86
|
-
},
|
|
87
|
-
s && {
|
|
88
|
-
type: "italic",
|
|
89
|
-
match: s,
|
|
90
|
-
index: s.index
|
|
91
|
-
}
|
|
92
|
-
].filter(Boolean);
|
|
93
|
-
if (c.length === 0) {
|
|
94
|
-
t.push(n);
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
c.sort((e, t) => e.index - t.index);
|
|
98
|
-
let l = c[0];
|
|
99
|
-
switch (l.index > 0 && t.push(n.slice(0, l.index)), l.type) {
|
|
100
|
-
case "bold":
|
|
101
|
-
t.push(/* @__PURE__ */ o("strong", {
|
|
102
|
-
className: "font-semibold",
|
|
103
|
-
children: l.match[1]
|
|
104
|
-
}, r++)), n = n.slice(l.index + l.match[0].length);
|
|
105
|
-
break;
|
|
106
|
-
case "code":
|
|
107
|
-
t.push(/* @__PURE__ */ o("code", {
|
|
108
|
-
className: "px-1 py-0.5 bg-bg-sunken text-text-primary rounded text-[0.85em] font-mono",
|
|
109
|
-
children: l.match[1]
|
|
110
|
-
}, r++)), n = n.slice(l.index + l.match[0].length);
|
|
111
|
-
break;
|
|
112
|
-
case "link":
|
|
113
|
-
t.push(/* @__PURE__ */ o("a", {
|
|
114
|
-
href: l.match[2],
|
|
115
|
-
target: "_blank",
|
|
116
|
-
rel: "noopener noreferrer",
|
|
117
|
-
className: "text-action-primary-bg hover:underline",
|
|
118
|
-
children: l.match[1]
|
|
119
|
-
}, r++)), n = n.slice(l.index + l.match[0].length);
|
|
120
|
-
break;
|
|
121
|
-
case "italic":
|
|
122
|
-
t.push(/* @__PURE__ */ o("em", {
|
|
123
|
-
className: "italic",
|
|
124
|
-
children: l.match[1]
|
|
125
|
-
}, r++)), n = n.slice(l.index + l.match[0].length);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return t;
|
|
130
|
-
}
|
|
131
|
-
function d({ content: e }) {
|
|
132
|
-
return /* @__PURE__ */ o(a, { children: e.split("\n").map((e, t) => {
|
|
133
|
-
let n = e.trimStart();
|
|
134
|
-
if (n.startsWith("### ")) return /* @__PURE__ */ o("h4", {
|
|
135
|
-
className: "text-sm font-semibold text-text-primary mt-3 mb-1",
|
|
136
|
-
children: u(n.slice(4))
|
|
137
|
-
}, t);
|
|
138
|
-
if (n.startsWith("## ")) return /* @__PURE__ */ o("h3", {
|
|
139
|
-
className: "text-sm font-bold text-text-primary mt-3 mb-1",
|
|
140
|
-
children: u(n.slice(3))
|
|
141
|
-
}, t);
|
|
142
|
-
if (n.startsWith("# ")) return /* @__PURE__ */ o("h2", {
|
|
143
|
-
className: "text-base font-bold text-text-primary mt-3 mb-1",
|
|
144
|
-
children: u(n.slice(2))
|
|
145
|
-
}, t);
|
|
146
|
-
if (/^[-*_]{3,}$/.test(n)) return /* @__PURE__ */ o("hr", { className: "my-2 border-border-default" }, t);
|
|
147
|
-
if (n.startsWith("> ")) return /* @__PURE__ */ o("blockquote", {
|
|
148
|
-
className: "border-l-2 border-action-primary-bg pl-3 text-text-secondary italic my-1",
|
|
149
|
-
children: u(n.slice(2))
|
|
150
|
-
}, t);
|
|
151
|
-
if (/^[-*] /.test(n)) return /* @__PURE__ */ o("li", {
|
|
152
|
-
className: "ml-4 list-disc text-text-primary",
|
|
153
|
-
children: u(n.slice(2))
|
|
154
|
-
}, t);
|
|
155
|
-
let r = n.match(/^(\d+)\.\s/);
|
|
156
|
-
return r ? /* @__PURE__ */ o("li", {
|
|
157
|
-
className: "ml-4 list-decimal text-text-primary",
|
|
158
|
-
children: u(n.slice(r[0].length))
|
|
159
|
-
}, t) : n === "" ? /* @__PURE__ */ o("div", { className: "h-1.5" }, t) : /* @__PURE__ */ o("p", {
|
|
160
|
-
className: "text-text-primary leading-relaxed",
|
|
161
|
-
children: u(e)
|
|
162
|
-
}, t);
|
|
163
|
-
}) });
|
|
164
|
-
}
|
|
165
|
-
function f({ content: e }) {
|
|
166
|
-
let t = e.split("\n").filter(Boolean);
|
|
167
|
-
if (t.length < 2) return null;
|
|
168
|
-
let n = (e) => e.split("|").map((e) => e.trim()).filter(Boolean), r = n(t[0]), i = t.slice(2).map(n);
|
|
169
|
-
return /* @__PURE__ */ o("div", {
|
|
170
|
-
className: "my-2 overflow-x-auto",
|
|
171
|
-
children: /* @__PURE__ */ s("table", {
|
|
172
|
-
className: "text-xs w-full border-collapse",
|
|
173
|
-
children: [/* @__PURE__ */ o("thead", { children: /* @__PURE__ */ o("tr", {
|
|
174
|
-
className: "border-b border-border-default",
|
|
175
|
-
children: r.map((e, t) => /* @__PURE__ */ o("th", {
|
|
176
|
-
className: "text-left px-2 py-1.5 font-semibold text-text-primary",
|
|
177
|
-
children: u(e)
|
|
178
|
-
}, t))
|
|
179
|
-
}) }), /* @__PURE__ */ o("tbody", { children: i.map((e, t) => /* @__PURE__ */ o("tr", {
|
|
180
|
-
className: "border-b border-border-subtle",
|
|
181
|
-
children: e.map((e, t) => /* @__PURE__ */ o("td", {
|
|
182
|
-
className: "px-2 py-1.5 text-text-primary",
|
|
183
|
-
children: u(e)
|
|
184
|
-
}, t))
|
|
185
|
-
}, t)) })]
|
|
186
|
-
})
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
var p = ({ content: e }) => /* @__PURE__ */ o("div", {
|
|
190
|
-
className: "text-xs leading-relaxed space-y-0.5",
|
|
191
|
-
children: t(() => l(e), [e]).map((e) => {
|
|
192
|
-
switch (e.type) {
|
|
193
|
-
case "code": return /* @__PURE__ */ o(c, {
|
|
194
|
-
language: e.language || "",
|
|
195
|
-
code: e.content
|
|
196
|
-
}, e.key);
|
|
197
|
-
case "table": return /* @__PURE__ */ o(f, { content: e.content }, e.key);
|
|
198
|
-
default: return /* @__PURE__ */ o(d, { content: e.content }, e.key);
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
});
|
|
202
|
-
//#endregion
|
|
203
|
-
export { p as AssistantMarkdownRenderer };
|
|
204
|
-
|
|
205
|
-
//# sourceMappingURL=AssistantMarkdownRenderer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantMarkdownRenderer.js","names":[],"sources":["../../../src/components/assistant/AssistantMarkdownRenderer.tsx"],"sourcesContent":["/**\n * Lightweight markdown renderer for assistant messages.\n *\n * Handles: headings, bold, italic, code blocks, inline code, links,\n * lists, blockquotes, tables, and horizontal rules using regex parsing.\n * No external dependencies — avoids adding react-markdown to the MFE.\n */\n\nimport React, { useMemo, useState, type FC } from 'react';\nimport { Copy, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface Props {\n content: string;\n}\n\nfunction CodeBlock({ language, code }: { language: string; code: string }) {\n const tr = useTr();\n const [copied, setCopied] = useState(false);\n\n const handleCopy = () => {\n navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div className=\"group relative my-2 rounded-md bg-bg-sunken border border-border-default overflow-hidden\">\n <div className=\"flex items-center justify-between px-3 py-1.5 bg-bg-elevated border-b border-border-default\">\n <span className=\"text-[10px] font-mono text-text-muted uppercase tracking-wider\">\n {language || 'code'}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded transition-colors\"\n aria-label={tr('vibecontrols.assistant.markdown.copyCode', 'Copy code')}\n >\n {copied ? (\n <>\n <Check className=\"size-3 text-status-success-text\" />\n <span>{tr('vibecontrols.assistant.markdown.copied', 'Copied')}</span>\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n <span>{tr('vibecontrols.assistant.markdown.copy', 'Copy')}</span>\n </>\n )}\n </button>\n </div>\n <pre className=\"p-3 overflow-x-auto text-xs leading-relaxed\">\n <code className=\"font-mono text-text-primary\">{code}</code>\n </pre>\n </div>\n );\n}\n\ninterface ParsedBlock {\n type: 'text' | 'code' | 'table';\n content: string;\n language?: string;\n key: number;\n}\n\nfunction parseBlocks(content: string): ParsedBlock[] {\n const blocks: ParsedBlock[] = [];\n let key = 0;\n\n // Split by code blocks first\n const parts = content.split(/(```[\\s\\S]*?```)/g);\n\n for (const part of parts) {\n const codeMatch = part.match(/^```(\\w*)\\n?([\\s\\S]*?)```$/);\n if (codeMatch) {\n blocks.push({\n type: 'code',\n language: codeMatch[1],\n content: codeMatch[2].trimEnd(),\n key: key++,\n });\n } else if (part.trim()) {\n // Check for tables\n const lines = part.split('\\n');\n let i = 0;\n let textBuffer = '';\n\n while (i < lines.length) {\n // Detect table: line with pipes followed by separator line\n if (lines[i].includes('|') && i + 1 < lines.length && /^\\|?[\\s-:|]+\\|/.test(lines[i + 1])) {\n if (textBuffer.trim()) {\n blocks.push({ type: 'text', content: textBuffer.trim(), key: key++ });\n textBuffer = '';\n }\n let tableContent = '';\n while (i < lines.length && lines[i].includes('|')) {\n tableContent += lines[i] + '\\n';\n i++;\n }\n blocks.push({ type: 'table', content: tableContent.trim(), key: key++ });\n } else {\n textBuffer += lines[i] + '\\n';\n i++;\n }\n }\n\n if (textBuffer.trim()) {\n blocks.push({ type: 'text', content: textBuffer.trim(), key: key++ });\n }\n }\n }\n\n return blocks;\n}\n\nfunction renderInlineMarkdown(text: string): (string | React.JSX.Element)[] {\n const result: (string | React.JSX.Element)[] = [];\n let remaining = text;\n let key = 0;\n\n while (remaining.length > 0) {\n // Bold\n const boldMatch = remaining.match(/\\*\\*(.+?)\\*\\*/);\n // Inline code\n const codeMatch = remaining.match(/`([^`]+)`/);\n // Link\n const linkMatch = remaining.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/);\n // Italic\n const italicMatch = remaining.match(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/);\n\n // Find the earliest match\n const matches = [\n boldMatch && { type: 'bold', match: boldMatch, index: boldMatch.index! },\n codeMatch && { type: 'code', match: codeMatch, index: codeMatch.index! },\n linkMatch && { type: 'link', match: linkMatch, index: linkMatch.index! },\n italicMatch && { type: 'italic', match: italicMatch, index: italicMatch.index! },\n ].filter(Boolean) as Array<{ type: string; match: RegExpMatchArray; index: number }>;\n\n if (matches.length === 0) {\n result.push(remaining);\n break;\n }\n\n matches.sort((a, b) => a.index - b.index);\n const earliest = matches[0];\n\n // Add text before the match\n if (earliest.index > 0) {\n result.push(remaining.slice(0, earliest.index));\n }\n\n switch (earliest.type) {\n case 'bold':\n result.push(\n <strong key={key++} className=\"font-semibold\">\n {earliest.match[1]}\n </strong>\n );\n remaining = remaining.slice(earliest.index + earliest.match[0].length);\n break;\n case 'code':\n result.push(\n <code\n key={key++}\n className=\"px-1 py-0.5 bg-bg-sunken text-text-primary rounded text-[0.85em] font-mono\"\n >\n {earliest.match[1]}\n </code>\n );\n remaining = remaining.slice(earliest.index + earliest.match[0].length);\n break;\n case 'link':\n result.push(\n <a\n key={key++}\n href={earliest.match[2]}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-action-primary-bg hover:underline\"\n >\n {earliest.match[1]}\n </a>\n );\n remaining = remaining.slice(earliest.index + earliest.match[0].length);\n break;\n case 'italic':\n result.push(\n <em key={key++} className=\"italic\">\n {earliest.match[1]}\n </em>\n );\n remaining = remaining.slice(earliest.index + earliest.match[0].length);\n break;\n }\n }\n\n return result;\n}\n\nfunction TextBlock({ content }: { content: string }) {\n const lines = content.split('\\n');\n\n return (\n <>\n {lines.map((line, i) => {\n const trimmed = line.trimStart();\n\n // Headings\n if (trimmed.startsWith('### '))\n return (\n <h4 key={i} className=\"text-sm font-semibold text-text-primary mt-3 mb-1\">\n {renderInlineMarkdown(trimmed.slice(4))}\n </h4>\n );\n if (trimmed.startsWith('## '))\n return (\n <h3 key={i} className=\"text-sm font-bold text-text-primary mt-3 mb-1\">\n {renderInlineMarkdown(trimmed.slice(3))}\n </h3>\n );\n if (trimmed.startsWith('# '))\n return (\n <h2 key={i} className=\"text-base font-bold text-text-primary mt-3 mb-1\">\n {renderInlineMarkdown(trimmed.slice(2))}\n </h2>\n );\n\n // Horizontal rule\n if (/^[-*_]{3,}$/.test(trimmed))\n return <hr key={i} className=\"my-2 border-border-default\" />;\n\n // Blockquote\n if (trimmed.startsWith('> '))\n return (\n <blockquote\n key={i}\n className=\"border-l-2 border-action-primary-bg pl-3 text-text-secondary italic my-1\"\n >\n {renderInlineMarkdown(trimmed.slice(2))}\n </blockquote>\n );\n\n // Unordered list\n if (/^[-*] /.test(trimmed))\n return (\n <li key={i} className=\"ml-4 list-disc text-text-primary\">\n {renderInlineMarkdown(trimmed.slice(2))}\n </li>\n );\n\n // Ordered list\n const olMatch = trimmed.match(/^(\\d+)\\.\\s/);\n if (olMatch)\n return (\n <li key={i} className=\"ml-4 list-decimal text-text-primary\">\n {renderInlineMarkdown(trimmed.slice(olMatch[0].length))}\n </li>\n );\n\n // Empty line\n if (trimmed === '') return <div key={i} className=\"h-1.5\" />;\n\n // Regular paragraph\n return (\n <p key={i} className=\"text-text-primary leading-relaxed\">\n {renderInlineMarkdown(line)}\n </p>\n );\n })}\n </>\n );\n}\n\nfunction TableBlock({ content }: { content: string }) {\n const rows = content.split('\\n').filter(Boolean);\n if (rows.length < 2) return null;\n\n const parseRow = (row: string) =>\n row\n .split('|')\n .map((c) => c.trim())\n .filter(Boolean);\n\n const headers = parseRow(rows[0]);\n // Skip separator row (index 1)\n const bodyRows = rows.slice(2).map(parseRow);\n\n return (\n <div className=\"my-2 overflow-x-auto\">\n <table className=\"text-xs w-full border-collapse\">\n <thead>\n <tr className=\"border-b border-border-default\">\n {headers.map((h, i) => (\n <th key={i} className=\"text-left px-2 py-1.5 font-semibold text-text-primary\">\n {renderInlineMarkdown(h)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {bodyRows.map((row, ri) => (\n <tr key={ri} className=\"border-b border-border-subtle\">\n {row.map((cell, ci) => (\n <td key={ci} className=\"px-2 py-1.5 text-text-primary\">\n {renderInlineMarkdown(cell)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nexport const AssistantMarkdownRenderer: FC<Props> = ({ content }) => {\n const blocks = useMemo(() => parseBlocks(content), [content]);\n\n return (\n <div className=\"text-xs leading-relaxed space-y-0.5\">\n {blocks.map((block) => {\n switch (block.type) {\n case 'code':\n return (\n <CodeBlock key={block.key} language={block.language || ''} code={block.content} />\n );\n case 'table':\n return <TableBlock key={block.key} content={block.content} />;\n default:\n return <TextBlock key={block.key} content={block.content} />;\n }\n })}\n </div>\n );\n};\n"],"mappings":"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"}
|