@burdenoff/microfe-vibecontrols 2026.529.1 → 2026.529.3
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/gitops/VibeGitOpsTab.js +4 -4
- package/dist/components/gitops/VibeGitOpsTab.js.map +1 -1
- package/dist/components/sessions/TerminalIframe.js +22 -0
- package/dist/components/sessions/TerminalIframe.js.map +1 -0
- package/dist/components/sessions/TerminalPanel.js +343 -338
- package/dist/components/sessions/TerminalPanel.js.map +1 -1
- package/dist/hooks/useTerminalCapabilityUrl.js +55 -0
- package/dist/hooks/useTerminalCapabilityUrl.js.map +1 -0
- package/dist/pages/SessionsPage.js +8 -3
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/services/iframeToken.js +5 -1
- package/dist/services/iframeToken.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { useVibeControls as e } from "../../providers/VibeControlsProvider.js";
|
|
|
2
2
|
import "../../constants/pluginCatalog.js";
|
|
3
3
|
import { PluginGate as t } from "../shared/PluginGate.js";
|
|
4
4
|
import "../shared/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { appendIframeTokenQuery as n, mintIframeToken as r, scheduleIframeTokenRefresh as i } from "../../services/iframeToken.js";
|
|
6
6
|
import { RepoStatsTab as a } from "./RepoStatsTab.js";
|
|
7
7
|
import { CITab as o } from "./CITab.js";
|
|
8
8
|
import { useGitopsContributors as ee, useGitopsIssues as te, useGitopsLanguages as ne, useGitopsPipelineAnalytics as re, useGitopsProviders as ie, useGitopsPullRequests as ae, useGitopsRepos as s, useGitopsRuns as oe, useGitopsSecurityAlerts as c } from "../../hooks/useGitops.js";
|
|
@@ -109,10 +109,10 @@ var b = ({ vibeId: b, agentId: C, profile: w, tunnelUrl: T, apiKey: E, tr: D })
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
try {
|
|
112
|
-
let a = await r(C, w, "/
|
|
112
|
+
let a = await r(C, w, "/ungit");
|
|
113
113
|
if (e) return;
|
|
114
|
-
let o = `${T.replace(/\/$/, "")}/
|
|
115
|
-
Q(n(o, a.token)), t = i(C, w, "/
|
|
114
|
+
let o = `${T.replace(/\/$/, "")}/ungit/`;
|
|
115
|
+
Q(n(o, a.token)), t = i(C, w, "/ungit", a, (e) => {
|
|
116
116
|
Q(n(o, e.token));
|
|
117
117
|
});
|
|
118
118
|
} catch (t) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VibeGitOpsTab.js","names":[],"sources":["../../../src/components/gitops/VibeGitOpsTab.tsx"],"sourcesContent":["/**\n * VibeGitOpsTab — GitOps tab content for the Vibe detail page.\n *\n * V2: All data is sourced from the agent over the tunnel via\n * `/api/profiles/<profile>/gitops/<provider>/*`. No backend GraphQL\n * involvement. The backend's old `gitopsProvider` records are deprecated.\n *\n * Sub-tabs:\n * 1. Repo Stats — repo metadata + PR + issue + security counts\n * 2. CI Pipelines — recent runs + analytics percentiles\n * 3. Git UI — Ungit iframe (when vibe-plugin-tool-git is installed)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { GitMerge, BarChart3, Play, GitBranch, RefreshCw } from 'lucide-react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { RepoStatsTab } from './RepoStatsTab';\nimport { CITab } from './CITab';\nimport { GitOpsSetup } from './GitOpsSetup';\nimport { GitOpsProviderPicker } from './GitOpsProviderPicker';\nimport { PluginGate } from '@/components/shared';\nimport { GIT_PACKAGE } from '@/constants/pluginCatalog';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport {\n useGitopsContributors,\n useGitopsIssues,\n useGitopsLanguages,\n useGitopsPipelineAnalytics,\n useGitopsProviders,\n useGitopsPullRequests,\n useGitopsRepos,\n useGitopsRuns,\n useGitopsSecurityAlerts,\n} from '@/hooks/useGitops';\nimport { composeCiStatus, composeRepoStats, type ProviderName, type RepoFqn } from './types';\nimport {\n mintIframeToken,\n scheduleIframeTokenRefresh,\n appendIframeTokenFragment,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\n\ninterface VibeGitOpsTabProps {\n vibeId: string;\n agentId: string;\n /** Canonical agent profile name. */\n profile: string;\n tunnelUrl: string | null;\n apiKey: string | null;\n tr: (key: string, fallback: string) => string;\n}\n\ntype GitOpsSubTab = 'repo-stats' | 'ci' | 'git-ui';\n\nexport const VibeGitOpsTab: FC<VibeGitOpsTabProps> = ({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n apiKey,\n tr,\n}) => {\n const [subTab, setSubTab] = useState<GitOpsSubTab>('repo-stats');\n const [showSetup, setShowSetup] = useState(false);\n const [selectedProvider, setSelectedProvider] = useState<ProviderName | null>(null);\n const [selectedRepoFqn, setSelectedRepoFqn] = useState<RepoFqn | null>(null);\n const { workspaceId } = useVibeControls();\n const qc = useQueryClient();\n\n const ctx = useMemo(\n () => ({ tunnelUrl, apiKey, profile, workspaceId: workspaceId ?? '', agentId }),\n [tunnelUrl, apiKey, profile, workspaceId, agentId]\n );\n\n // Providers registered on the agent.\n const providersQ = useGitopsProviders(ctx);\n const authedProviders = useMemo(\n () => providersQ.data?.filter((p) => p.registered && p.authenticated) ?? [],\n [providersQ.data]\n );\n const hasProviders = authedProviders.length > 0;\n\n // Pick a default once data loads.\n useEffect(() => {\n if (!selectedProvider && authedProviders.length > 0) {\n const first = authedProviders[0];\n if (first) {\n setSelectedProvider(first.name as ProviderName);\n }\n }\n }, [authedProviders, selectedProvider]);\n\n // Once a provider is selected, fetch repos for it and pick the first one\n // by default. The user can switch repos via the picker.\n const reposQ = useGitopsRepos(ctx, selectedProvider, { limit: 30 });\n useEffect(() => {\n if (!selectedRepoFqn && reposQ.data?.items.length) {\n const first = reposQ.data.items[0];\n if (first) {\n setSelectedRepoFqn(first.fqn);\n }\n }\n }, [reposQ.data, selectedRepoFqn]);\n\n // Repo Stats composite — runs only when active sub-tab is repo-stats.\n const repoQ = useGitopsRepos(ctx, selectedProvider, { limit: 1 }); // metadata via list (1st item)\n const repoDetailQ = useGitopsRepos.length ? null : null; // keep TS happy; we use repoQ.items[0]\n const pullsQ = useGitopsPullRequests(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const issuesQ = useGitopsIssues(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const languagesQ = useGitopsLanguages(ctx, selectedProvider, selectedRepoFqn);\n const contributorsQ = useGitopsContributors(ctx, selectedProvider, selectedRepoFqn, 20);\n const securityQ = useGitopsSecurityAlerts(ctx, selectedProvider, selectedRepoFqn);\n\n const activeRepo = useMemo(\n () =>\n selectedRepoFqn\n ? (repoQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n reposQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n null)\n : (repoQ.data?.items[0] ?? null),\n [repoQ.data, reposQ.data, selectedRepoFqn]\n );\n\n const repoStats = useMemo(\n () =>\n composeRepoStats({\n repo: activeRepo,\n pulls: pullsQ.data,\n issues: issuesQ.data,\n languages: languagesQ.data,\n contributors: contributorsQ.data,\n securityAlerts: securityQ.data,\n }),\n [activeRepo, pullsQ.data, issuesQ.data, languagesQ.data, contributorsQ.data, securityQ.data]\n );\n\n // CI Status composite.\n const runsQ = useGitopsRuns(ctx, selectedProvider, selectedRepoFqn, { limit: 30 });\n const ciAnalyticsQ = useGitopsPipelineAnalytics(ctx, selectedProvider, selectedRepoFqn);\n const ciStatus = useMemo(\n () =>\n composeCiStatus({\n pipelines: undefined,\n runs: runsQ.data,\n analytics: ciAnalyticsQ.data,\n }),\n [runsQ.data, ciAnalyticsQ.data]\n );\n\n const repoStatsLoading = pullsQ.isLoading || issuesQ.isLoading || languagesQ.isLoading;\n const ciStatusLoading = runsQ.isLoading || ciAnalyticsQ.isLoading;\n const repoStatsError = (pullsQ.error || issuesQ.error || languagesQ.error)?.message;\n const ciStatusError = (runsQ.error || ciAnalyticsQ.error)?.message;\n\n const handleRefresh = useCallback(() => {\n void qc.invalidateQueries({ queryKey: ['gitops', workspaceId ?? '', agentId, profile] });\n }, [qc, workspaceId, agentId, profile]);\n\n const isRefreshing = pullsQ.isFetching || runsQ.isFetching;\n\n const subTabs: { id: GitOpsSubTab; label: string; icon: typeof BarChart3 }[] = [\n { id: 'repo-stats', label: tr('gitops.repoStats', 'Repo Stats'), icon: BarChart3 },\n { id: 'ci', label: tr('gitops.ci', 'CI Pipelines'), icon: Play },\n { id: 'git-ui', label: tr('gitops.gitUI', 'Git UI'), icon: GitBranch },\n ];\n\n // Ungit iframe state (unchanged from v1).\n const [ungitIframeUrl, setUngitIframeUrl] = useState<string | null>(null);\n const [ungitTokenError, setUngitTokenError] = useState<string | null>(null);\n useEffect(() => {\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n const setup = async () => {\n if (!agentId || !tunnelUrl || subTab !== 'git-ui') {\n setUngitIframeUrl(null);\n return;\n }\n try {\n // F5 residual hard-cutover — token is now carried as a `#vt=`\n // URL fragment (not a `?vt=` query param). The agent's host-\n // level cookie-shim at `/api/plugins/<plugin>/ui` reads the\n // fragment client-side, POSTs it to `/api/agent/ui-exchange-\n // plugin`, and sets the path-scoped `vt_ui_ungit` HttpOnly\n // cookie. Fragments never reach the server, never land in\n // proxy access logs, and aren't sent on outbound sub-request\n // Referer headers — matches the F5 `/ui/<plugin>` flow.\n const initial: IframeTokenResponse = await mintIframeToken(\n agentId,\n profile,\n '/api/plugins/ungit/ui'\n );\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/api/plugins/ungit/ui`;\n setUngitIframeUrl(appendIframeTokenFragment(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/api/plugins/ungit/ui',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenFragment(baseUrl, next.token));\n }\n );\n } catch (err) {\n if (!cancelled) {\n setUngitTokenError(err instanceof Error ? err.message : 'Failed to fetch iframe token');\n }\n }\n };\n void setup();\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, tunnelUrl, subTab]);\n\n return (\n <motion.div\n key=\"gitops\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-4\"\n data-vibe-id={vibeId}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <GitMerge className=\"size-5 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibeDetails.gitopsTitle', 'GitOps')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n {hasProviders && (\n <GitOpsProviderPicker\n providers={authedProviders.map((p) => p.name as ProviderName)}\n selected={selectedProvider}\n onChange={setSelectedProvider}\n />\n )}\n {hasProviders && (subTab === 'repo-stats' || subTab === 'ci') && (\n <button\n type=\"button\"\n onClick={handleRefresh}\n disabled={isRefreshing}\n className=\"flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border-default text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50 transition-colors\"\n data-testid=\"gitops-refresh\"\n >\n <RefreshCw className={`size-3 ${isRefreshing ? 'animate-spin' : ''}`} />\n {tr('gitops.refresh', 'Refresh')}\n </button>\n )}\n {!showSetup && (\n <button\n type=\"button\"\n onClick={() => setShowSetup(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n data-testid=\"gitops-add-provider\"\n >\n {hasProviders\n ? tr('gitops.addAnotherProvider', 'Add Provider')\n : tr('gitops.addProvider', 'Add Provider')}\n </button>\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col gap-4 lg:flex-row lg:gap-6\">\n <nav\n className=\"w-full flex-shrink-0 rounded-lg border border-border-default bg-bg-surface p-2 lg:w-56\"\n role=\"tablist\"\n aria-label={tr('gitops.navigation', 'GitOps navigation')}\n >\n {subTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = subTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setSubTab(tab.id)}\n className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${\n isActive\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary'\n }`}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n <div className=\"min-w-0 flex-1 space-y-4\">\n {showSetup && (\n <GitOpsSetup\n ctx={ctx}\n onCreated={() => {\n setShowSetup(false);\n void providersQ.refetch();\n }}\n onCancel={() => setShowSetup(false)}\n />\n )}\n\n {subTab === 'repo-stats' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={GitMerge}\n message=\"gitops.noProvidersVibe\"\n fallback=\"No GitOps provider configured. Add a PAT to see repository stats.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !tunnelUrl || !apiKey ? (\n <Notice\n tr={tr}\n message=\"gitops.noTunnel\"\n fallback=\"Waiting for agent tunnel. Make sure the agent is running and a tunnel is active.\"\n />\n ) : (\n <RepoStatsTab\n stats={repoStats}\n loading={repoStatsLoading || providersQ.isLoading}\n error={repoStatsError}\n />\n )}\n </>\n )}\n\n {subTab === 'ci' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={Play}\n message=\"gitops.noProvidersCi\"\n fallback=\"No GitOps provider configured. Add a PAT to see CI pipeline status.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !tunnelUrl || !apiKey ? (\n <Notice tr={tr} message=\"gitops.noTunnelCi\" fallback=\"Waiting for agent tunnel.\" />\n ) : (\n <CITab\n status={ciStatus}\n loading={ciStatusLoading || providersQ.isLoading}\n error={ciStatusError}\n />\n )}\n </>\n )}\n\n {subTab === 'git-ui' && (\n <PluginGate\n agentId={agentId || null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('gitops.ungitEyebrow', 'Ungit plugin required')}\n title={tr('gitops.ungitTitle', 'Install the visual git client')}\n description={tr(\n 'gitops.ungitDescription',\n 'The Git UI embeds the Ungit visual git client served by your agent. Install the plugin to continue.'\n )}\n >\n {ungitTokenError ? (\n <div className=\"rounded-lg border border-border-default bg-status-error-bg-subtle p-4\">\n <p className=\"text-sm text-status-error-text\">{ungitTokenError}</p>\n </div>\n ) : ungitIframeUrl ? (\n <iframe\n src={ungitIframeUrl}\n title=\"Ungit\"\n className=\"w-full min-h-[600px] rounded-lg border border-border-default\"\n />\n ) : (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr('gitops.ungitLoading', 'Loading Ungit…')}\n </p>\n </div>\n )}\n </PluginGate>\n )}\n </div>\n </div>\n </motion.div>\n );\n};\n\ninterface EmptyStateProps {\n tr: (key: string, fallback: string) => string;\n icon: typeof BarChart3;\n message: string;\n fallback: string;\n ctaLabel?: string;\n onCta?: () => void;\n}\n\nconst EmptyState: FC<EmptyStateProps> = ({\n tr,\n icon: Icon,\n message,\n fallback,\n ctaLabel,\n onCta,\n}) => (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-6 text-center\">\n <Icon className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n {ctaLabel && onCta && (\n <button\n type=\"button\"\n onClick={onCta}\n className=\"mt-3 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {ctaLabel}\n </button>\n )}\n </div>\n);\n\ninterface NoticeProps {\n tr: (key: string, fallback: string) => string;\n message: string;\n fallback: string;\n}\n\nconst Notice: FC<NoticeProps> = ({ tr, message, fallback }) => (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n </div>\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VibeGitOpsTab.js","names":[],"sources":["../../../src/components/gitops/VibeGitOpsTab.tsx"],"sourcesContent":["/**\n * VibeGitOpsTab — GitOps tab content for the Vibe detail page.\n *\n * V2: All data is sourced from the agent over the tunnel via\n * `/api/profiles/<profile>/gitops/<provider>/*`. No backend GraphQL\n * involvement. The backend's old `gitopsProvider` records are deprecated.\n *\n * Sub-tabs:\n * 1. Repo Stats — repo metadata + PR + issue + security counts\n * 2. CI Pipelines — recent runs + analytics percentiles\n * 3. Git UI — Ungit iframe (when vibe-plugin-tool-git is installed)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { GitMerge, BarChart3, Play, GitBranch, RefreshCw } from 'lucide-react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { RepoStatsTab } from './RepoStatsTab';\nimport { CITab } from './CITab';\nimport { GitOpsSetup } from './GitOpsSetup';\nimport { GitOpsProviderPicker } from './GitOpsProviderPicker';\nimport { PluginGate } from '@/components/shared';\nimport { GIT_PACKAGE } from '@/constants/pluginCatalog';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport {\n useGitopsContributors,\n useGitopsIssues,\n useGitopsLanguages,\n useGitopsPipelineAnalytics,\n useGitopsProviders,\n useGitopsPullRequests,\n useGitopsRepos,\n useGitopsRuns,\n useGitopsSecurityAlerts,\n} from '@/hooks/useGitops';\nimport { composeCiStatus, composeRepoStats, type ProviderName, type RepoFqn } from './types';\nimport {\n mintIframeToken,\n scheduleIframeTokenRefresh,\n appendIframeTokenQuery,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\n\ninterface VibeGitOpsTabProps {\n vibeId: string;\n agentId: string;\n /** Canonical agent profile name. */\n profile: string;\n tunnelUrl: string | null;\n apiKey: string | null;\n tr: (key: string, fallback: string) => string;\n}\n\ntype GitOpsSubTab = 'repo-stats' | 'ci' | 'git-ui';\n\nexport const VibeGitOpsTab: FC<VibeGitOpsTabProps> = ({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n apiKey,\n tr,\n}) => {\n const [subTab, setSubTab] = useState<GitOpsSubTab>('repo-stats');\n const [showSetup, setShowSetup] = useState(false);\n const [selectedProvider, setSelectedProvider] = useState<ProviderName | null>(null);\n const [selectedRepoFqn, setSelectedRepoFqn] = useState<RepoFqn | null>(null);\n const { workspaceId } = useVibeControls();\n const qc = useQueryClient();\n\n const ctx = useMemo(\n () => ({ tunnelUrl, apiKey, profile, workspaceId: workspaceId ?? '', agentId }),\n [tunnelUrl, apiKey, profile, workspaceId, agentId]\n );\n\n // Providers registered on the agent.\n const providersQ = useGitopsProviders(ctx);\n const authedProviders = useMemo(\n () => providersQ.data?.filter((p) => p.registered && p.authenticated) ?? [],\n [providersQ.data]\n );\n const hasProviders = authedProviders.length > 0;\n\n // Pick a default once data loads.\n useEffect(() => {\n if (!selectedProvider && authedProviders.length > 0) {\n const first = authedProviders[0];\n if (first) {\n setSelectedProvider(first.name as ProviderName);\n }\n }\n }, [authedProviders, selectedProvider]);\n\n // Once a provider is selected, fetch repos for it and pick the first one\n // by default. The user can switch repos via the picker.\n const reposQ = useGitopsRepos(ctx, selectedProvider, { limit: 30 });\n useEffect(() => {\n if (!selectedRepoFqn && reposQ.data?.items.length) {\n const first = reposQ.data.items[0];\n if (first) {\n setSelectedRepoFqn(first.fqn);\n }\n }\n }, [reposQ.data, selectedRepoFqn]);\n\n // Repo Stats composite — runs only when active sub-tab is repo-stats.\n const repoQ = useGitopsRepos(ctx, selectedProvider, { limit: 1 }); // metadata via list (1st item)\n const repoDetailQ = useGitopsRepos.length ? null : null; // keep TS happy; we use repoQ.items[0]\n const pullsQ = useGitopsPullRequests(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const issuesQ = useGitopsIssues(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const languagesQ = useGitopsLanguages(ctx, selectedProvider, selectedRepoFqn);\n const contributorsQ = useGitopsContributors(ctx, selectedProvider, selectedRepoFqn, 20);\n const securityQ = useGitopsSecurityAlerts(ctx, selectedProvider, selectedRepoFqn);\n\n const activeRepo = useMemo(\n () =>\n selectedRepoFqn\n ? (repoQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n reposQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n null)\n : (repoQ.data?.items[0] ?? null),\n [repoQ.data, reposQ.data, selectedRepoFqn]\n );\n\n const repoStats = useMemo(\n () =>\n composeRepoStats({\n repo: activeRepo,\n pulls: pullsQ.data,\n issues: issuesQ.data,\n languages: languagesQ.data,\n contributors: contributorsQ.data,\n securityAlerts: securityQ.data,\n }),\n [activeRepo, pullsQ.data, issuesQ.data, languagesQ.data, contributorsQ.data, securityQ.data]\n );\n\n // CI Status composite.\n const runsQ = useGitopsRuns(ctx, selectedProvider, selectedRepoFqn, { limit: 30 });\n const ciAnalyticsQ = useGitopsPipelineAnalytics(ctx, selectedProvider, selectedRepoFqn);\n const ciStatus = useMemo(\n () =>\n composeCiStatus({\n pipelines: undefined,\n runs: runsQ.data,\n analytics: ciAnalyticsQ.data,\n }),\n [runsQ.data, ciAnalyticsQ.data]\n );\n\n const repoStatsLoading = pullsQ.isLoading || issuesQ.isLoading || languagesQ.isLoading;\n const ciStatusLoading = runsQ.isLoading || ciAnalyticsQ.isLoading;\n const repoStatsError = (pullsQ.error || issuesQ.error || languagesQ.error)?.message;\n const ciStatusError = (runsQ.error || ciAnalyticsQ.error)?.message;\n\n const handleRefresh = useCallback(() => {\n void qc.invalidateQueries({ queryKey: ['gitops', workspaceId ?? '', agentId, profile] });\n }, [qc, workspaceId, agentId, profile]);\n\n const isRefreshing = pullsQ.isFetching || runsQ.isFetching;\n\n const subTabs: { id: GitOpsSubTab; label: string; icon: typeof BarChart3 }[] = [\n { id: 'repo-stats', label: tr('gitops.repoStats', 'Repo Stats'), icon: BarChart3 },\n { id: 'ci', label: tr('gitops.ci', 'CI Pipelines'), icon: Play },\n { id: 'git-ui', label: tr('gitops.gitUI', 'Git UI'), icon: GitBranch },\n ];\n\n // Ungit iframe state (unchanged from v1).\n const [ungitIframeUrl, setUngitIframeUrl] = useState<string | null>(null);\n const [ungitTokenError, setUngitTokenError] = useState<string | null>(null);\n useEffect(() => {\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n const setup = async () => {\n if (!agentId || !tunnelUrl || subTab !== 'git-ui') {\n setUngitIframeUrl(null);\n return;\n }\n try {\n // F5 residual — Ungit's reverse proxy lives at `/ungit/` (declared\n // by the external `vibe-plugin-tool-git` package's publicPaths +\n // meta.url override). The plugin's createUngitProxy validates the\n // entry-document auth via `?vt=<token>` (or `?apiKey=`), then\n // issues its own session cookie for sub-requests. Switching this\n // to a host-level hash→cookie shim (like F5 does for `/ui/<p>`)\n // would require an SDK contract change + coordinated republish of\n // the external plugin package. Until that ships, the iframe-token\n // sits in the URL once. Mitigations in place: F6 makes it single-\n // use (replayed token rejected); F7 strips the token before\n // upstream forwarding so Ungit never sees it; F10 enforces Origin\n // on non-GET/HEAD sub-requests. Residual surface: one query string\n // appears in CF logs + browser history for the initial document.\n const initial: IframeTokenResponse = await mintIframeToken(agentId, profile, '/ungit');\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/ungit/`;\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/ungit',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, next.token));\n }\n );\n } catch (err) {\n if (!cancelled) {\n setUngitTokenError(err instanceof Error ? err.message : 'Failed to fetch iframe token');\n }\n }\n };\n void setup();\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, tunnelUrl, subTab]);\n\n return (\n <motion.div\n key=\"gitops\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-4\"\n data-vibe-id={vibeId}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <GitMerge className=\"size-5 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibeDetails.gitopsTitle', 'GitOps')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n {hasProviders && (\n <GitOpsProviderPicker\n providers={authedProviders.map((p) => p.name as ProviderName)}\n selected={selectedProvider}\n onChange={setSelectedProvider}\n />\n )}\n {hasProviders && (subTab === 'repo-stats' || subTab === 'ci') && (\n <button\n type=\"button\"\n onClick={handleRefresh}\n disabled={isRefreshing}\n className=\"flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border-default text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50 transition-colors\"\n data-testid=\"gitops-refresh\"\n >\n <RefreshCw className={`size-3 ${isRefreshing ? 'animate-spin' : ''}`} />\n {tr('gitops.refresh', 'Refresh')}\n </button>\n )}\n {!showSetup && (\n <button\n type=\"button\"\n onClick={() => setShowSetup(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n data-testid=\"gitops-add-provider\"\n >\n {hasProviders\n ? tr('gitops.addAnotherProvider', 'Add Provider')\n : tr('gitops.addProvider', 'Add Provider')}\n </button>\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col gap-4 lg:flex-row lg:gap-6\">\n <nav\n className=\"w-full flex-shrink-0 rounded-lg border border-border-default bg-bg-surface p-2 lg:w-56\"\n role=\"tablist\"\n aria-label={tr('gitops.navigation', 'GitOps navigation')}\n >\n {subTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = subTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setSubTab(tab.id)}\n className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${\n isActive\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary'\n }`}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n <div className=\"min-w-0 flex-1 space-y-4\">\n {showSetup && (\n <GitOpsSetup\n ctx={ctx}\n onCreated={() => {\n setShowSetup(false);\n void providersQ.refetch();\n }}\n onCancel={() => setShowSetup(false)}\n />\n )}\n\n {subTab === 'repo-stats' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={GitMerge}\n message=\"gitops.noProvidersVibe\"\n fallback=\"No GitOps provider configured. Add a PAT to see repository stats.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !tunnelUrl || !apiKey ? (\n <Notice\n tr={tr}\n message=\"gitops.noTunnel\"\n fallback=\"Waiting for agent tunnel. Make sure the agent is running and a tunnel is active.\"\n />\n ) : (\n <RepoStatsTab\n stats={repoStats}\n loading={repoStatsLoading || providersQ.isLoading}\n error={repoStatsError}\n />\n )}\n </>\n )}\n\n {subTab === 'ci' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={Play}\n message=\"gitops.noProvidersCi\"\n fallback=\"No GitOps provider configured. Add a PAT to see CI pipeline status.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !tunnelUrl || !apiKey ? (\n <Notice tr={tr} message=\"gitops.noTunnelCi\" fallback=\"Waiting for agent tunnel.\" />\n ) : (\n <CITab\n status={ciStatus}\n loading={ciStatusLoading || providersQ.isLoading}\n error={ciStatusError}\n />\n )}\n </>\n )}\n\n {subTab === 'git-ui' && (\n <PluginGate\n agentId={agentId || null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('gitops.ungitEyebrow', 'Ungit plugin required')}\n title={tr('gitops.ungitTitle', 'Install the visual git client')}\n description={tr(\n 'gitops.ungitDescription',\n 'The Git UI embeds the Ungit visual git client served by your agent. Install the plugin to continue.'\n )}\n >\n {ungitTokenError ? (\n <div className=\"rounded-lg border border-border-default bg-status-error-bg-subtle p-4\">\n <p className=\"text-sm text-status-error-text\">{ungitTokenError}</p>\n </div>\n ) : ungitIframeUrl ? (\n <iframe\n src={ungitIframeUrl}\n title=\"Ungit\"\n className=\"w-full min-h-[600px] rounded-lg border border-border-default\"\n />\n ) : (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr('gitops.ungitLoading', 'Loading Ungit…')}\n </p>\n </div>\n )}\n </PluginGate>\n )}\n </div>\n </div>\n </motion.div>\n );\n};\n\ninterface EmptyStateProps {\n tr: (key: string, fallback: string) => string;\n icon: typeof BarChart3;\n message: string;\n fallback: string;\n ctaLabel?: string;\n onCta?: () => void;\n}\n\nconst EmptyState: FC<EmptyStateProps> = ({\n tr,\n icon: Icon,\n message,\n fallback,\n ctaLabel,\n onCta,\n}) => (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-6 text-center\">\n <Icon className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n {ctaLabel && onCta && (\n <button\n type=\"button\"\n onClick={onCta}\n className=\"mt-3 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {ctaLabel}\n </button>\n )}\n </div>\n);\n\ninterface NoticeProps {\n tr: (key: string, fallback: string) => string;\n message: string;\n fallback: string;\n}\n\nconst Notice: FC<NoticeProps> = ({ tr, message, fallback }) => (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n </div>\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useTerminalCapabilityUrl as e } from "../../hooks/useTerminalCapabilityUrl.js";
|
|
2
|
+
import { forwardRef as t } from "react";
|
|
3
|
+
import { Fragment as n, jsx as r } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/sessions/TerminalIframe.tsx
|
|
5
|
+
var i = t(function({ sessionId: t, agentId: i, baseTtydUrl: a, title: o, className: s, allow: c, onLoad: l, onError: u, fallback: d }, f) {
|
|
6
|
+
let { url: p, remint: m } = e(i, t, a);
|
|
7
|
+
return p ? /* @__PURE__ */ r("iframe", {
|
|
8
|
+
ref: f,
|
|
9
|
+
src: p,
|
|
10
|
+
title: o,
|
|
11
|
+
className: s,
|
|
12
|
+
allow: c,
|
|
13
|
+
onLoad: l,
|
|
14
|
+
onError: () => {
|
|
15
|
+
m(), u?.();
|
|
16
|
+
}
|
|
17
|
+
}) : /* @__PURE__ */ r(n, { children: d ?? null });
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { i as TerminalIframe };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=TerminalIframe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TerminalIframe.js","names":[],"sources":["../../../src/components/sessions/TerminalIframe.tsx"],"sourcesContent":["/**\n * TerminalIframe — renders the ttyd iframe with an F3-compliant\n * capability token appended as a `#vt=<token>` URL fragment.\n *\n * Each tab in the sessions terminal panel needs its own capability mint\n * (the iframe-token is scoped to `/terminal/<sessionId>`). React hooks\n * can't be called in a `tabs.map(...)` loop, so the iframe rendering is\n * extracted here and the hook runs per component instance.\n *\n * Stale browser tabs and reopened sessions hit a 10-minute cookie TTL\n * inside the agent; `onError` triggers `remint()` which re-fetches a\n * fresh token and re-iframes.\n */\nimport { type ReactNode, forwardRef } from 'react';\n\nimport { useTerminalCapabilityUrl } from '@/hooks/useTerminalCapabilityUrl';\n\nexport interface TerminalIframeProps {\n /** Session id — used as resourceId for the capability mint. */\n sessionId: string;\n /** Agent id owning this session — required for the mint mutation. */\n agentId: string | null | undefined;\n /** Bare ttyd tunnel URL from `session.ttydUrl`. Hook strips any legacy ?apiKey=. */\n baseTtydUrl: string | null | undefined;\n /** Title for accessibility. */\n title: string;\n /** Optional className for the iframe. */\n className?: string;\n /** Permissions-Policy allow attribute (clipboard). */\n allow?: string;\n /** Called on iframe load. Use to flip connection status to connected. */\n onLoad?: () => void;\n /** Called on iframe error. We re-mint internally + bubble the event. */\n onError?: () => void;\n /** Slot for fallback content when no URL is available yet. */\n fallback?: ReactNode;\n}\n\nexport const TerminalIframe = forwardRef<HTMLIFrameElement, TerminalIframeProps>(\n function TerminalIframe(\n { sessionId, agentId, baseTtydUrl, title, className, allow, onLoad, onError, fallback },\n ref\n ) {\n const { url, remint } = useTerminalCapabilityUrl(agentId, sessionId, baseTtydUrl);\n\n if (!url) {\n return <>{fallback ?? null}</>;\n }\n\n return (\n <iframe\n ref={ref}\n src={url}\n title={title}\n className={className}\n allow={allow}\n onLoad={onLoad}\n onError={() => {\n // Re-mint on any iframe error — covers both 401 (cookie expired\n // mid-session) and network blips.\n remint();\n onError?.();\n }}\n />\n );\n }\n);\n"],"mappings":";;;;AAsCA,IAAa,IAAiB,EAC5B,SACE,EAAE,cAAW,YAAS,gBAAa,UAAO,cAAW,UAAO,WAAQ,YAAS,eAC7E,GACA;CACA,IAAM,EAAE,QAAK,cAAW,EAAyB,GAAS,GAAW,EAAY;AAMjF,QAJK,IAKH,kBAAC,UAAD;EACO;EACL,KAAK;EACE;EACI;EACJ;EACC;EACR,eAAe;AAIb,GADA,GAAQ,EACR,KAAW;;EAEb,CAAA,GAjBK,kBAAA,GAAA,EAAA,UAAG,KAAY,MAAQ,CAAA;EAoBnC"}
|