@talgent/workbench-ui 0.0.0 → 0.2.0
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/README.md +15 -2
- package/assets/brand-icons/LICENSE.md +30 -0
- package/assets/brand-icons/README.md +5 -0
- package/assets/brand-icons/figma.svg +1 -0
- package/assets/brand-icons/github.svg +1 -0
- package/assets/brand-icons/linear.svg +1 -0
- package/assets/brand-icons/notion.svg +1 -0
- package/assets/brand-icons/sentry.svg +1 -0
- package/assets/brand-icons/slack.svg +1 -0
- package/assets/fonts/Inter-OFL.txt +93 -0
- package/assets/fonts/inter-latin.woff2 +0 -0
- package/components/agent/agent-icon.css +14 -0
- package/components/agent/agent-icon.tsx +14 -0
- package/components/analytics/chart-geometry.ts +95 -0
- package/components/analytics/dashboard-charts.tsx +136 -0
- package/components/analytics/model-usage-chart.tsx +84 -0
- package/components/analytics/token-breakdown.css +13 -0
- package/components/analytics/token-breakdown.tsx +23 -0
- package/components/analytics/token-report.tsx +57 -0
- package/components/analytics/work-usage/turn-timeline.tsx +98 -0
- package/components/analytics/work-usage/turns.ts +170 -0
- package/components/analytics/work-usage/work-status.ts +36 -0
- package/components/analytics/work-usage/work-usage-reader.tsx +122 -0
- package/components/analytics/work-usage/work-usage.css +48 -0
- package/components/auth/auth-gate.tsx +44 -0
- package/components/chat/message-input.tsx +105 -0
- package/components/composer/permission-mode-list.tsx +15 -0
- package/components/composer/prompt-bar-controls.tsx +77 -0
- package/components/composer/prompt-bar.tsx +45 -0
- package/components/composer/skill-completion.css +27 -0
- package/components/composer/skill-completion.tsx +49 -0
- package/components/composer/use-slash-completion.tsx +69 -0
- package/components/composer/work-start.css +133 -0
- package/components/feedback/toast-region.css +2 -0
- package/components/feedback/toast-region.tsx +15 -0
- package/components/files/file-facts.tsx +16 -0
- package/components/files/file-section.tsx +181 -0
- package/components/markdown.tsx +144 -0
- package/components/marketplace/connector-pages.tsx +166 -0
- package/components/marketplace/marketplace-icons.tsx +66 -0
- package/components/marketplace/marketplace-shared.tsx +43 -0
- package/components/marketplace/marketplace-shell.tsx +32 -0
- package/components/marketplace/marketplace.css +97 -0
- package/components/marketplace/parameter-fields.tsx +46 -0
- package/components/marketplace/plugin-list.tsx +84 -0
- package/components/marketplace/plugin-page.tsx +133 -0
- package/components/marketplace/provider-pages.tsx +120 -0
- package/components/preview/file-format.ts +75 -0
- package/components/preview/file-preview.tsx +153 -0
- package/components/preview/preview-verdict.ts +44 -0
- package/components/preview/preview.css +19 -0
- package/components/preview/text-preview.tsx +174 -0
- package/components/profile/provider-callback.tsx +43 -0
- package/components/profile/work-agent-appearance.tsx +109 -0
- package/components/profile/work-agent-soul.tsx +76 -0
- package/components/rail/rail-section.tsx +20 -0
- package/components/rail/rail.css +19 -0
- package/components/rail/use-expand-on-change.ts +18 -0
- package/components/session/startup-progress.css +77 -0
- package/components/session/startup-progress.tsx +103 -0
- package/components/session/work-permission-control.tsx +45 -0
- package/components/session/work-thinking-control.tsx +52 -0
- package/components/shell/account-menu.tsx +134 -0
- package/components/shell/app-shell.tsx +148 -0
- package/components/shell/core-reading-views.tsx +26 -0
- package/components/shell/core-reading.tsx +17 -0
- package/components/shell/document-title.tsx +11 -0
- package/components/shell/entry-shell.ts +39 -0
- package/components/shell/file-reading.tsx +22 -0
- package/components/shell/page-layout.tsx +7 -0
- package/components/shell/reading-context.ts +41 -0
- package/components/shell/reading-panel.css +67 -0
- package/components/shell/reading-panel.tsx +74 -0
- package/components/shell/sidebar-parts.tsx +38 -0
- package/components/work/context-rail.css +61 -0
- package/components/work/conversation-unavailable.css +6 -0
- package/components/work/conversation-unavailable.tsx +11 -0
- package/components/work/subagent-item.tsx +42 -0
- package/components/work/subagents-section.css +31 -0
- package/components/work/subagents-section.tsx +40 -0
- package/components/work/token-usage.tsx +28 -0
- package/components/work/tool-message-detail.tsx +39 -0
- package/components/work/tool-output-preview.tsx +52 -0
- package/components/work/tool-preview-reading.tsx +27 -0
- package/components/work/tool-reading-content.tsx +31 -0
- package/components/work/work-context-rail.tsx +23 -0
- package/components/work/work-conversation.tsx +207 -0
- package/components/work/work-history.tsx +115 -0
- package/components/work/work-interaction.css +38 -0
- package/components/work/work-interaction.tsx +105 -0
- package/components/work/work-message-flow.css +86 -0
- package/components/work/work-message-flow.tsx +352 -0
- package/components/work/work-progress-section.tsx +20 -0
- package/components/work/work-prompt-bar.tsx +82 -0
- package/components/work/work-question-record.tsx +72 -0
- package/components/work/work-snapshot-state.tsx +24 -0
- package/components/work/work-status-control.tsx +54 -0
- package/components/work-start/start-work-launch-state.ts +33 -0
- package/components/work-start/work-start-catalog.tsx +116 -0
- package/components/work-start/work-start-form.tsx +173 -0
- package/components/work-start/work-start-tray.css +40 -0
- package/components/work-start/work-start-tray.tsx +128 -0
- package/hooks/use-debounced.ts +11 -0
- package/hooks/use-work-conversation.ts +125 -0
- package/hooks/use-work-lifecycle-controls.ts +40 -0
- package/hooks/use-work-stream.ts +91 -0
- package/lib/account-service.ts +77 -0
- package/lib/agent-appearance-service.ts +59 -0
- package/lib/agent-icon.ts +26 -0
- package/lib/agent-soul-service.ts +48 -0
- package/lib/agent-work-service.ts +23 -0
- package/lib/client.ts +20 -0
- package/lib/computer-release.ts +30 -0
- package/lib/computer-service.ts +241 -0
- package/lib/dashboard-report.ts +38 -0
- package/lib/dashboard.ts +21 -0
- package/lib/marketplace-catalog.ts +138 -0
- package/lib/marketplace-connections.ts +98 -0
- package/lib/marketplace-plugins.ts +53 -0
- package/lib/marketplace-routes.ts +12 -0
- package/lib/model-vendors.ts +13 -0
- package/lib/name-taken-error.ts +7 -0
- package/lib/notifications.ts +37 -0
- package/lib/permission-modes.ts +25 -0
- package/lib/personal-usage-service.ts +19 -0
- package/lib/personal-work-service.ts +159 -0
- package/lib/plugin-parameters.ts +64 -0
- package/lib/preview-verdict.ts +26 -0
- package/lib/relative-time.ts +11 -0
- package/lib/session.ts +108 -0
- package/lib/share-link.ts +4 -0
- package/lib/stale-build-reload.ts +41 -0
- package/lib/start-work-request.ts +34 -0
- package/lib/startup-progress.ts +9 -0
- package/lib/subagents.ts +12 -0
- package/lib/thinking-levels.ts +2 -0
- package/lib/token-analytics.ts +168 -0
- package/lib/token-usage.ts +64 -0
- package/lib/trace-view-model.ts +292 -0
- package/lib/transport.ts +29 -0
- package/lib/use-now.ts +11 -0
- package/lib/use-work-trace.ts +175 -0
- package/lib/user-display.ts +49 -0
- package/lib/work-conversation.ts +118 -0
- package/lib/work-file-service.ts +118 -0
- package/lib/work-model.ts +38 -0
- package/lib/work-notices.ts +23 -0
- package/lib/work-progress.ts +41 -0
- package/lib/work-projection.ts +122 -0
- package/lib/work-questions.ts +18 -0
- package/lib/work-start-draft.ts +61 -0
- package/lib/work-start-loadout.ts +100 -0
- package/lib/work-start-source.ts +16 -0
- package/lib/work-state-presentation.ts +80 -0
- package/lib/work-stream-source.ts +32 -0
- package/package.json +81 -5
- package/pages/computers/computers-page.tsx +56 -0
- package/pages/computers/connect-computer-page.tsx +92 -0
- package/pages/computers/install-commands.tsx +42 -0
- package/pages/computers/remove-computer-dialog.tsx +33 -0
- package/pages/computers/rename-computer-dialog.tsx +37 -0
- package/pages/computers/room-rules-dialog.tsx +55 -0
- package/pages/login-page.css +33 -0
- package/pages/login-page.tsx +88 -0
- package/pages/login-threads.tsx +51 -0
- package/pages/marketplace-page.tsx +59 -0
- package/pages/profile-page.tsx +68 -0
- package/pages/workbench/library-page.tsx +88 -0
- package/pages/workbench/new-work-page.tsx +24 -0
- package/pages/workbench/person.tsx +13 -0
- package/pages/workbench/personal-work-context.tsx +93 -0
- package/pages/workbench/personal-work-page.tsx +100 -0
- package/pages/workbench/personal-works-page.tsx +36 -0
- package/pages/workbench/share-dialog.tsx +57 -0
- package/pages/workbench/title-dialog.tsx +35 -0
- package/pages/workbench/usage-page.tsx +52 -0
- package/public/assets/brand/talgent-lettering.png +0 -0
- package/public/assets/brand/talgent-logo-page-bg.png +0 -0
- package/public/assets/brand/talgent-logo-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark-page-bg.png +0 -0
- package/public/assets/brand/talgent-mark-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark.png +0 -0
- package/public/assets/product-icons/LICENSE +21 -0
- package/public/assets/product-icons/README.md +13 -0
- package/public/assets/product-icons/anthropic.svg +1 -0
- package/public/assets/product-icons/deepseek.svg +1 -0
- package/public/assets/product-icons/glm.svg +1 -0
- package/public/assets/product-icons/kimi.svg +1 -0
- package/public/assets/product-icons/linear.svg +1 -0
- package/public/assets/product-icons/minimax.svg +1 -0
- package/public/assets/product-icons/notion.svg +1 -0
- package/public/assets/product-icons/openai.svg +1 -0
- package/public/assets/product-icons/qwen.svg +1 -0
- package/styles/base.css +95 -0
- package/styles/fonts.css +14 -0
- package/styles/markdown.css +79 -0
- package/styles/uber-theme.css +96 -0
- package/styles/work-composer.css +222 -0
- package/styles/work-stream.css +343 -0
- package/styles/workbench.css +144 -0
- package/styles/workspace.css +260 -0
- package/test-support/backend.ts +67 -0
- package/test-support/plugin-summary.ts +8 -0
- package/test-support/setup.ts +59 -0
- package/vite-preset.ts +85 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Tabs } from "@heroui/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
4
|
+
import { PageLayout } from "../shell/page-layout";
|
|
5
|
+
import { marketplacePaths } from "../../lib/marketplace-routes";
|
|
6
|
+
import "./marketplace.css";
|
|
7
|
+
|
|
8
|
+
export type Crumb = { label: string; to: string };
|
|
9
|
+
|
|
10
|
+
const sections = [{ id: "plugins", label: "Plugins", to: marketplacePaths.plugins }, { id: "connectors", label: "Connectors", to: marketplacePaths.connectors }] as const;
|
|
11
|
+
|
|
12
|
+
// Marketplace is the Plugin and Connector catalog only: its sections
|
|
13
|
+
// switch in the topbar, what the person installed is in the account menu, and
|
|
14
|
+
// Provider Authorizations are on Profile. A Provider page keeps this shell
|
|
15
|
+
// with Profile as its parent and no catalog switch.
|
|
16
|
+
export function MarketplaceShell({ title, trail = [], root = { label: "Marketplace", to: marketplacePaths.plugins }, children }: {
|
|
17
|
+
title: string; trail?: Crumb[]; root?: Crumb; children: ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const { pathname } = useLocation();
|
|
21
|
+
const current = pathname.startsWith(marketplacePaths.connectors) ? "connectors" : "plugins";
|
|
22
|
+
// The switch shows on the two lists only; a category or detail page leads back by its breadcrumb.
|
|
23
|
+
const onList = pathname === marketplacePaths.plugins || pathname === marketplacePaths.connectors;
|
|
24
|
+
const tabs = <Tabs className="p-perspective-tabs" selectedKey={current} onSelectionChange={key => navigate(sections.find(section => section.id === key)!.to)}>
|
|
25
|
+
<Tabs.ListContainer><Tabs.List aria-label="Marketplace section">{sections.map(section => <Tabs.Tab key={section.id} id={section.id}>{section.label}<Tabs.Indicator /></Tabs.Tab>)}</Tabs.List></Tabs.ListContainer>
|
|
26
|
+
</Tabs>;
|
|
27
|
+
return <PageLayout workspaceTitle={title} centerActions={onList ? tabs : undefined} trail={[root, ...trail].map(crumb => ({ label: crumb.label, onPress: () => navigate(crumb.to) }))}>
|
|
28
|
+
<div className="mk-market">
|
|
29
|
+
<div className="mk-main">{children}</div>
|
|
30
|
+
</div>
|
|
31
|
+
</PageLayout>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* Marketplace: a secondary rail beside quiet two-column lists; every
|
|
2
|
+
object is a page. HeroUI owns component appearance; this file owns layout,
|
|
3
|
+
capability hues and the icon tiles. */
|
|
4
|
+
:root {
|
|
5
|
+
--mk-skill: oklch(55% 0.075 210);
|
|
6
|
+
--mk-agent: oklch(52% 0.1 295);
|
|
7
|
+
--mk-mcp: oklch(57% 0.09 70);
|
|
8
|
+
--mk-soft: color-mix(in oklch, var(--foreground) 5%, var(--surface));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.mk-cap { flex-shrink: 0; }
|
|
12
|
+
.mk-cap-skill { color: var(--mk-skill); }
|
|
13
|
+
.mk-cap-agent { color: var(--mk-agent); }
|
|
14
|
+
.mk-cap-mcp { color: var(--mk-mcp); }
|
|
15
|
+
.mk-muted { color: var(--muted); }
|
|
16
|
+
.mk-hint { color: var(--muted); font-weight: 400; }
|
|
17
|
+
|
|
18
|
+
.mk-market { display: grid; padding: 8px 24px 96px 4px; }
|
|
19
|
+
.mk-main { display: grid; gap: 28px; min-width: 0; width: 100%; max-width: 1080px; margin-inline: auto; }
|
|
20
|
+
.mk-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
|
21
|
+
.mk-search { width: min(420px, 100%); }
|
|
22
|
+
.mk-section { display: grid; gap: 8px; }
|
|
23
|
+
.mk-section-head { display: inline-flex; align-items: center; gap: 4px; justify-self: start; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; border-radius: 6px; }
|
|
24
|
+
a.mk-section-head:hover svg { transform: translateX(2px); }
|
|
25
|
+
.mk-section-head svg { color: var(--muted); transition: transform 150ms; }
|
|
26
|
+
.mk-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 40px; }
|
|
27
|
+
.mk-row { display: flex; align-items: center; gap: 12px; min-height: 72px; }
|
|
28
|
+
.mk-row-open { flex: 1; min-width: 0; display: flex; align-items: center; gap: 14px; padding: 10px 8px; margin-left: -8px; border-radius: 14px; text-align: left; }
|
|
29
|
+
.mk-row-open:focus-visible { outline: 2px solid var(--focus); }
|
|
30
|
+
.mk-row-text { display: grid; gap: 2px; min-width: 0; }
|
|
31
|
+
.mk-row-text strong { display: inline-flex; align-items: center; gap: 6px; font-size: 15px; font-weight: 500; }
|
|
32
|
+
.mk-row-text span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: var(--muted); }
|
|
33
|
+
.mk-row[data-off="true"] .mk-row-text strong { color: var(--muted); }
|
|
34
|
+
.mk-row-lock { color: var(--muted); padding: 8px; }
|
|
35
|
+
.mk-empty { display: grid; justify-items: center; gap: 6px; padding: 40px 16px; text-align: center; font-size: 13px; }
|
|
36
|
+
.mk-skeletons { display: grid; gap: 12px; }
|
|
37
|
+
.mk-skeleton { height: 72px; border-radius: 16px; }
|
|
38
|
+
|
|
39
|
+
.mk-icon { flex-shrink: 0; display: grid; place-items: center; background: color-mix(in oklch, var(--mk-hue) 13%, var(--surface)); color: color-mix(in oklch, var(--mk-hue) 88%, var(--foreground)); box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--mk-hue) 14%, transparent); }
|
|
40
|
+
.mk-icon-brand { background: var(--surface); box-shadow: inset 0 0 0 1px var(--separator), 0 1px 2px oklch(0 0 0 / 0.04); }
|
|
41
|
+
.mk-brand-mark { display: block; mask-size: contain; mask-repeat: no-repeat; mask-position: center; -webkit-mask-size: contain; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; }
|
|
42
|
+
.mk-icon-letter { font-weight: 600; }
|
|
43
|
+
.mk-icon-stack { position: relative; flex-shrink: 0; display: inline-grid; }
|
|
44
|
+
.mk-icon-badge { position: absolute; right: -4px; bottom: -4px; display: grid; place-items: center; padding: 2px; border-radius: 50%; background: var(--surface); color: var(--mk-mcp); box-shadow: 0 0 0 1px var(--separator); }
|
|
45
|
+
|
|
46
|
+
/* Item pages (Plugin, Connector, Provider) share one anatomy. */
|
|
47
|
+
.mk-detail { display: grid; gap: 20px; max-width: 880px; }
|
|
48
|
+
.mk-detail-head { display: flex; align-items: flex-start; gap: 20px; }
|
|
49
|
+
.mk-detail-title { flex: 1; min-width: 0; display: grid; gap: 6px; }
|
|
50
|
+
.mk-detail-title h2 { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
|
|
51
|
+
.mk-meta { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; color: var(--muted); }
|
|
52
|
+
.mk-meta > span + span::before { content: ""; display: inline-block; width: 3px; height: 3px; margin: 0 10px 3px -8px; border-radius: 50%; background: currentColor; opacity: 0.5; vertical-align: middle; }
|
|
53
|
+
.mk-detail-actions { flex-shrink: 0; display: flex; align-items: center; gap: 12px; padding-top: 4px; }
|
|
54
|
+
.mk-inline { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
|
|
55
|
+
.mk-section-title { font-size: 15px; font-weight: 600; }
|
|
56
|
+
.mk-section-title .mk-count { margin-left: 6px; font-weight: 400; color: var(--muted); }
|
|
57
|
+
.mk-detail .mk-section { gap: 10px; }
|
|
58
|
+
.mk-kind { margin-left: 6px; padding: 1px 6px; border-radius: 6px; background: var(--mk-soft); font-size: 11px; font-weight: 500; color: var(--muted); vertical-align: 1px; }
|
|
59
|
+
.mk-group { padding: 4px 18px; gap: 0; border-radius: 18px; }
|
|
60
|
+
.mk-group-list { list-style: none; margin: 0; padding: 0; }
|
|
61
|
+
.mk-group-list > li { display: flex; align-items: flex-start; gap: 12px; padding: 14px 0; }
|
|
62
|
+
.mk-group-list > li + li { border-top: 1px solid var(--separator); }
|
|
63
|
+
.mk-group-list > li > .mk-cap { margin-top: 3px; }
|
|
64
|
+
.mk-group-list .mk-row-text span { white-space: normal; }
|
|
65
|
+
.mk-group-list code { font: 500 13.5px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
66
|
+
.mk-group-link { padding: 0 !important; }
|
|
67
|
+
.mk-group-link > a { display: flex; align-items: center; gap: 12px; width: calc(100% + 16px); margin: 0 -8px; padding: 12px 8px; border-radius: 12px; text-align: left; }
|
|
68
|
+
.mk-group-link > a:focus-visible { outline: 2px solid var(--focus); }
|
|
69
|
+
.mk-group-split { display: flex !important; align-items: center; gap: 12px; }
|
|
70
|
+
.mk-group-split > a { flex: 1; width: auto; margin-right: 0; min-width: 0; }
|
|
71
|
+
.mk-about { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, max-content); gap: 12px 32px; padding: 20px 22px; }
|
|
72
|
+
.mk-about-single { grid-template-columns: 1fr; }
|
|
73
|
+
.mk-about-main { display: grid; gap: 12px; align-content: start; }
|
|
74
|
+
.mk-about-text { font-size: 15px; line-height: 1.7; max-width: 60ch; }
|
|
75
|
+
.mk-about-facts { display: grid; gap: 8px; margin: 0; font-size: 13px; align-content: start; padding-left: 24px; border-left: 1px solid var(--separator); }
|
|
76
|
+
.mk-about-facts > div { display: flex; justify-content: space-between; gap: 24px; }
|
|
77
|
+
.mk-about-facts dt { color: var(--muted); }
|
|
78
|
+
.mk-about-facts dd { margin: 0; text-align: right; overflow-wrap: anywhere; }
|
|
79
|
+
.mk-keywords { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
80
|
+
.mk-settings-card { padding: 20px 22px; }
|
|
81
|
+
.mk-form { display: grid; gap: 16px; max-width: 560px; }
|
|
82
|
+
.mk-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
|
|
83
|
+
.mk-section-bar { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
84
|
+
.mk-section-actions { display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
|
|
85
|
+
.mk-manage { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; }
|
|
86
|
+
.mk-note { font-size: 13px; color: var(--muted); }
|
|
87
|
+
|
|
88
|
+
@media (max-width: 1100px) {
|
|
89
|
+
.mk-rows { grid-template-columns: 1fr; }
|
|
90
|
+
}
|
|
91
|
+
@media (max-width: 740px) {
|
|
92
|
+
.mk-about { grid-template-columns: 1fr; }
|
|
93
|
+
.mk-about-facts { padding: 12px 0 0; border-left: 0; border-top: 1px solid var(--separator); }
|
|
94
|
+
.mk-market { padding: 12px 16px 96px; }
|
|
95
|
+
.mk-detail-head { flex-wrap: wrap; }
|
|
96
|
+
.mk-detail-actions { width: 100%; }
|
|
97
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
2
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
3
|
+
import { Description, FieldError, Input, Label, ListBox, Select, Switch, TextArea, TextField } from "@heroui/react";
|
|
4
|
+
import type { ParameterDeclaration } from "@talgent/workbench-contracts/marketplace/public/v1/marketplace_pb";
|
|
5
|
+
import { hasDefault, parameterLabel, type ParameterDrafts, type ParameterErrors } from "../../lib/plugin-parameters";
|
|
6
|
+
|
|
7
|
+
function defaultText(declaration: ParameterDeclaration) {
|
|
8
|
+
if (declaration.sensitive || !declaration.default) return "";
|
|
9
|
+
const value = toJson(ValueSchema, declaration.default);
|
|
10
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Fields follow the published declarations. Sensitive values are never echoed:
|
|
14
|
+
// a saved one shows only that it exists and can be replaced.
|
|
15
|
+
export function ParameterFields({ declarations, drafts, errors = {}, redacted = [], inherited, onChange }: {
|
|
16
|
+
declarations: Record<string, ParameterDeclaration>; drafts: ParameterDrafts; errors?: ParameterErrors; redacted?: string[];
|
|
17
|
+
/** Personal values a Work override starts from, shown as a hint. */
|
|
18
|
+
inherited?: ParameterDrafts; onChange: (name: string, value: string) => void;
|
|
19
|
+
}) {
|
|
20
|
+
return <div className="mk-form">{Object.entries(declarations).map(([name, declaration]) => {
|
|
21
|
+
const fallback = defaultText(declaration);
|
|
22
|
+
const hint = inherited?.[name] !== undefined ? `Your default: ${inherited[name]}.` : fallback ? `Default: ${fallback}.` : "";
|
|
23
|
+
const optional = !declaration.required || hasDefault(declaration);
|
|
24
|
+
const label = <Label>{parameterLabel(name, declaration)}{optional && <span className="mk-hint"> (optional)</span>}</Label>;
|
|
25
|
+
const description = [declaration.description, hint, declaration.multiple ? "One value per line." : "", declaration.sensitive ? "Stored securely and never shown again." : ""].filter(Boolean).join(" ");
|
|
26
|
+
if (declaration.type === "boolean" && !declaration.multiple) {
|
|
27
|
+
const selected = (drafts[name] ?? inherited?.[name] ?? fallback) === "true";
|
|
28
|
+
return <Switch key={name} isSelected={selected} onChange={value => onChange(name, String(value))}>
|
|
29
|
+
<Switch.Control><Switch.Thumb /></Switch.Control><Switch.Content>{label}<Description>{description}</Description></Switch.Content>
|
|
30
|
+
</Switch>;
|
|
31
|
+
}
|
|
32
|
+
if (declaration.options.length && !declaration.multiple) {
|
|
33
|
+
return <Select key={name} name={name} value={drafts[name] || null} isInvalid={Boolean(errors[name])} onChange={value => onChange(name, String(value ?? ""))} placeholder="Choose an option" fullWidth variant="secondary">
|
|
34
|
+
{label}<Select.Trigger><Select.Value /><Select.Indicator /></Select.Trigger><Description>{description}</Description>{errors[name] && <FieldError>{errors[name]}</FieldError>}
|
|
35
|
+
<Select.Popover><ListBox>{declaration.options.map(option => <ListBox.Item key={option} id={option} textValue={option}><Label>{option}</Label><ListBox.ItemIndicator /></ListBox.Item>)}</ListBox></Select.Popover>
|
|
36
|
+
</Select>;
|
|
37
|
+
}
|
|
38
|
+
const saved = declaration.sensitive && redacted.includes(name);
|
|
39
|
+
return <TextField key={name} name={name} value={drafts[name] ?? ""} onChange={value => onChange(name, value)} isInvalid={Boolean(errors[name])} type={declaration.sensitive ? "password" : "text"} fullWidth variant="secondary">
|
|
40
|
+
{label}
|
|
41
|
+
{declaration.multiple ? <TextArea rows={3} placeholder={fallback} /> : <Input autoComplete="off" inputMode={declaration.type === "number" ? "decimal" : undefined} placeholder={saved ? "Saved — enter a new value to replace it" : fallback} />}
|
|
42
|
+
<Description>{description}</Description>
|
|
43
|
+
{errors[name] && <FieldError>{errors[name]}</FieldError>}
|
|
44
|
+
</TextField>;
|
|
45
|
+
})}</div>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Button, Dropdown, Label, Tooltip } from "@heroui/react";
|
|
3
|
+
import type { InstalledPlugin, PluginSummary } from "@talgent/workbench-contracts/marketplace/public/v1/marketplace_pb";
|
|
4
|
+
import { AlertTriangle, ChevronRight, Lock, MoreHorizontal, Plus } from "lucide-react";
|
|
5
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
6
|
+
import { useInstalledPlugins, usePluginActions, usePluginCatalog } from "../../lib/marketplace-catalog";
|
|
7
|
+
import { groupPluginsByCategory, isPlatformPlugin, pluginPresentation } from "../../lib/marketplace-plugins";
|
|
8
|
+
import { marketplacePaths } from "../../lib/marketplace-routes";
|
|
9
|
+
import { notify } from "../../lib/notifications";
|
|
10
|
+
import { installationNeedsSetup, missingRequired } from "../../lib/plugin-parameters";
|
|
11
|
+
import { PluginIcon } from "./marketplace-icons";
|
|
12
|
+
import { ConfirmDialog, LoadState, NothingMatches, SearchBar } from "./marketplace-shared";
|
|
13
|
+
|
|
14
|
+
const matches = (plugin: PluginSummary, query: string) => `${pluginPresentation(plugin).name} ${plugin.slug} ${plugin.description}`.toLowerCase().includes(query.trim().toLowerCase());
|
|
15
|
+
|
|
16
|
+
// Plugins grouped by purpose, "Other" last. A category page shows all of its
|
|
17
|
+
// Plugins; the overview shows the first six of each.
|
|
18
|
+
export function PluginList({ category }: { category?: string }) {
|
|
19
|
+
const catalog = usePluginCatalog();
|
|
20
|
+
const installed = useInstalledPlugins();
|
|
21
|
+
const [query, setQuery] = useState("");
|
|
22
|
+
const [removing, setRemoving] = useState<{ plugin: PluginSummary; installed: InstalledPlugin } | null>(null);
|
|
23
|
+
const actions = usePluginActions();
|
|
24
|
+
const groups = groupPluginsByCategory((catalog.data ?? []).filter(plugin => matches(plugin, query))).filter(group => !category || group.category === category);
|
|
25
|
+
const uninstall = (target: { plugin: PluginSummary; installed: InstalledPlugin }) => {
|
|
26
|
+
const name = pluginPresentation(target.plugin).name;
|
|
27
|
+
actions.uninstall.mutate(target.installed, {
|
|
28
|
+
onSuccess: () => notify(`plugin:${target.plugin.slug}`, "default", `${name} uninstalled`),
|
|
29
|
+
onError: () => notify(`plugin:${target.plugin.slug}`, "danger", `${name} couldn’t be uninstalled`, "Refresh and try again."),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
return <>
|
|
33
|
+
<SearchBar label={category ? `Search ${category.toLowerCase()} plugins` : "Search plugins"} value={query} onChange={setQuery} />
|
|
34
|
+
<LoadState noun="plugins" isPending={catalog.isPending} isError={catalog.isError} hasData={Boolean(catalog.data)} isFetching={catalog.isFetching} onRetry={() => void catalog.refetch()} />
|
|
35
|
+
{catalog.data && catalog.data.length === 0 && <div className="mk-empty"><p><strong>No plugins yet</strong></p><p className="mk-muted">Plugins appear here once the platform publishes them.</p></div>}
|
|
36
|
+
{catalog.data && catalog.data.length > 0 && (groups.length === 0 ? <NothingMatches onClear={() => setQuery("")} />
|
|
37
|
+
: groups.map(group => <section key={group.category} className="mk-section" aria-label={group.category}>
|
|
38
|
+
{!category && <Link className="mk-section-head" to={marketplacePaths.pluginCategory(group.category)}>{group.category}<ChevronRight size={18} aria-hidden="true" /></Link>}
|
|
39
|
+
<ul className="mk-rows">{(category ? group.plugins : group.plugins.slice(0, 6)).map(plugin => <PluginRow key={plugin.slug} plugin={plugin} installed={installed.data?.get(plugin.slug)} onUninstall={value => setRemoving({ plugin, installed: value })} />)}</ul>
|
|
40
|
+
</section>))}
|
|
41
|
+
<ConfirmDialog isOpen={removing !== null} onOpenChange={open => { if (!open) setRemoving(null); }} title={`Uninstall ${removing ? pluginPresentation(removing.plugin).name : ""}?`} confirmLabel="Uninstall" onConfirm={() => { if (removing) uninstall(removing); }}>
|
|
42
|
+
<p>Your saved settings are removed. Works already running keep it. Connectors and connected Providers aren’t affected.</p>
|
|
43
|
+
</ConfirmDialog>
|
|
44
|
+
</>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function PluginRow({ plugin, installed, onUninstall }: { plugin: PluginSummary; installed?: InstalledPlugin; onUninstall: (installed: InstalledPlugin) => void }) {
|
|
48
|
+
const navigate = useNavigate();
|
|
49
|
+
const actions = usePluginActions();
|
|
50
|
+
const { name } = pluginPresentation(plugin);
|
|
51
|
+
const needsSetup = installed ? installationNeedsSetup(plugin.parameters, installed) : false;
|
|
52
|
+
const install = () => {
|
|
53
|
+
// Required settings are entered on the Plugin page, never half-installed.
|
|
54
|
+
if (missingRequired(plugin.parameters, {}).length) { navigate(marketplacePaths.plugin(plugin.slug)); return; }
|
|
55
|
+
actions.install.mutate({ slug: plugin.slug, configuration: {} }, {
|
|
56
|
+
onSuccess: result => notify(`plugin:${plugin.slug}`, "success", `${name} installed`, "Works you start from now on include it.", {
|
|
57
|
+
action: { label: "Undo", onPress: () => actions.uninstall.mutate(result, { onSuccess: () => notify(`plugin:${plugin.slug}`, "default", `${name} uninstalled`) }) },
|
|
58
|
+
}),
|
|
59
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", `${name} couldn’t be installed`, "Try again from its page."),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const toggle = (value: InstalledPlugin) => actions.setEnabled.mutate({ installed: value, enabled: !value.enabled }, {
|
|
63
|
+
onSuccess: () => notify(`plugin:${plugin.slug}`, "default", `${name} ${value.enabled ? "disabled" : "enabled"}`),
|
|
64
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", `${name} couldn’t be changed`, "Refresh and try again."),
|
|
65
|
+
});
|
|
66
|
+
return <li className="mk-row" data-off={installed ? !installed.enabled : undefined}>
|
|
67
|
+
<Link className="mk-row-open" to={marketplacePaths.plugin(plugin.slug)}>
|
|
68
|
+
<PluginIcon plugin={plugin} />
|
|
69
|
+
<span className="mk-row-text"><strong>{name}{needsSetup && <AlertTriangle size={13} className="text-warning" aria-label="Needs attention" />}</strong><span>{installed && !installed.enabled ? "Disabled" : plugin.description}</span></span>
|
|
70
|
+
</Link>
|
|
71
|
+
{isPlatformPlugin(plugin.slug) ? <Tooltip><Tooltip.Trigger aria-label="Always enabled" className="mk-row-lock"><Lock size={16} /></Tooltip.Trigger><Tooltip.Content>Always enabled</Tooltip.Content></Tooltip>
|
|
72
|
+
: installed ? <Dropdown><Button size="sm" variant="ghost" isIconOnly aria-label={`${name} options`}><MoreHorizontal size={18} /></Button>
|
|
73
|
+
<Dropdown.Popover placement="bottom end"><Dropdown.Menu aria-label={`${name} options`} onAction={key => {
|
|
74
|
+
if (key === "open") navigate(marketplacePaths.plugin(plugin.slug));
|
|
75
|
+
if (key === "toggle") toggle(installed);
|
|
76
|
+
if (key === "uninstall") onUninstall(installed);
|
|
77
|
+
}}>
|
|
78
|
+
<Dropdown.Item id="open" textValue="Open"><Label>Open</Label></Dropdown.Item>
|
|
79
|
+
<Dropdown.Item id="toggle" textValue={installed.enabled ? "Disable" : "Enable"}><Label>{installed.enabled ? "Disable" : "Enable"}</Label></Dropdown.Item>
|
|
80
|
+
<Dropdown.Item id="uninstall" textValue="Uninstall" variant="danger"><Label>Uninstall…</Label></Dropdown.Item>
|
|
81
|
+
</Dropdown.Menu></Dropdown.Popover></Dropdown>
|
|
82
|
+
: <Button size="sm" variant="ghost" isIconOnly isPending={actions.install.isPending} aria-label={missingRequired(plugin.parameters, {}).length ? `Set up ${name}` : `Install ${name}`} onPress={install}><Plus size={20} strokeWidth={1.75} /></Button>}
|
|
83
|
+
</li>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Alert, Button, Card, Chip, Dropdown, Label, Link as HeroLink, Switch } from "@heroui/react";
|
|
3
|
+
import type { InstalledPlugin, PluginRelease } from "@talgent/workbench-contracts/marketplace/public/v1/marketplace_pb";
|
|
4
|
+
import { ChevronRight, Lock, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import { Link } from "react-router-dom";
|
|
6
|
+
import { useInstalledPlugins, usePluginActions } from "../../lib/marketplace-catalog";
|
|
7
|
+
import { isPlatformPlugin, pluginPresentation } from "../../lib/marketplace-plugins";
|
|
8
|
+
import { marketplacePaths } from "../../lib/marketplace-routes";
|
|
9
|
+
import { notify } from "../../lib/notifications";
|
|
10
|
+
import { draftsFrom, missingRequired, parameterLabel, parseParameters, type ParameterDrafts, type ParameterErrors } from "../../lib/plugin-parameters";
|
|
11
|
+
import { serviceLabel } from "../../lib/marketplace-connections";
|
|
12
|
+
import { ConnectorIcon, PluginIcon } from "./marketplace-icons";
|
|
13
|
+
import { CapIcon, ConfirmDialog } from "./marketplace-shared";
|
|
14
|
+
import { ParameterFields } from "./parameter-fields";
|
|
15
|
+
|
|
16
|
+
// Marketplace › Plugins › Name. The header carries identity and actions; the
|
|
17
|
+
// body explains what the Plugin brings into a Work. Skills and Agents appear
|
|
18
|
+
// only here, never in Marketplace lists.
|
|
19
|
+
export function PluginPage({ plugin }: { plugin: PluginRelease }) {
|
|
20
|
+
const installedPlugins = useInstalledPlugins();
|
|
21
|
+
const installed = installedPlugins.data?.get(plugin.slug);
|
|
22
|
+
const actions = usePluginActions();
|
|
23
|
+
const presentation = pluginPresentation(plugin);
|
|
24
|
+
const [drafts, setDrafts] = useState<ParameterDrafts>({});
|
|
25
|
+
const [errors, setErrors] = useState<ParameterErrors>({});
|
|
26
|
+
const [confirm, setConfirm] = useState(false);
|
|
27
|
+
const form = useRef<HTMLFormElement>(null);
|
|
28
|
+
const platform = isPlatformPlugin(plugin.slug);
|
|
29
|
+
const saved = { configuration: installed?.configuration, redacted: installed?.redactedParameters };
|
|
30
|
+
const missing = installed ? missingRequired(plugin.parameters, saved) : [];
|
|
31
|
+
const values = { ...draftsFrom(installed?.configuration), ...drafts };
|
|
32
|
+
const hasSettings = Object.keys(plugin.parameters).length > 0;
|
|
33
|
+
|
|
34
|
+
const submit = () => {
|
|
35
|
+
const parsed = parseParameters(plugin.parameters, drafts, saved);
|
|
36
|
+
setErrors(parsed.errors);
|
|
37
|
+
const invalid = Object.keys(parsed.errors)[0];
|
|
38
|
+
if (invalid) { form.current?.querySelector<HTMLElement>(`[name="${CSS.escape(invalid)}"]`)?.focus(); return; }
|
|
39
|
+
const name = presentation.name;
|
|
40
|
+
if (!installed) {
|
|
41
|
+
actions.install.mutate({ slug: plugin.slug, configuration: parsed.values }, {
|
|
42
|
+
onSuccess: result => { setDrafts({}); notify(`plugin:${plugin.slug}`, "success", `${name} installed`, "Works you start from now on include it.", { action: { label: "Undo", onPress: () => actions.uninstall.mutate(result) } }); },
|
|
43
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", `${name} couldn’t be installed`, "Check the settings and try again."),
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// A blank field that had a saved value clears that setting.
|
|
48
|
+
const cleared = Object.entries(drafts).filter(([key, value]) => !value.trim() && installed.configuration?.[key] !== undefined).map(([key]) => key);
|
|
49
|
+
actions.configure.mutate({ installed, values: parsed.values, cleared }, {
|
|
50
|
+
onSuccess: () => { setDrafts({}); notify(`plugin:${plugin.slug}`, "success", "Settings saved", "Works you start from now on use them."); },
|
|
51
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", "Settings couldn’t be saved", "Check the values and try again."),
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const toggle = (value: InstalledPlugin) => actions.setEnabled.mutate({ installed: value, enabled: !value.enabled }, {
|
|
55
|
+
onSuccess: () => notify(`plugin:${plugin.slug}`, "default", `${presentation.name} ${value.enabled ? "disabled" : "enabled"}`),
|
|
56
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", `${presentation.name} couldn’t be changed`, "Refresh and try again."),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return <div className="mk-detail">
|
|
60
|
+
<header className="mk-detail-head">
|
|
61
|
+
<PluginIcon plugin={plugin} size={64} />
|
|
62
|
+
<div className="mk-detail-title"><h2>{presentation.name}</h2><p className="mk-meta"><span>{presentation.category}</span><span>Version {plugin.version}</span></p></div>
|
|
63
|
+
<div className="mk-detail-actions">
|
|
64
|
+
{platform ? <span className="mk-muted mk-inline"><Lock size={15} aria-hidden="true" />Always enabled</span>
|
|
65
|
+
: !installed ? <Button variant="primary" isPending={actions.install.isPending} isDisabled={installedPlugins.isPending} onPress={submit}>Install</Button>
|
|
66
|
+
: <>
|
|
67
|
+
<Switch isSelected={installed.enabled} isDisabled={actions.setEnabled.isPending} onChange={() => toggle(installed)} aria-label={`Enable ${presentation.name}`}><Switch.Control><Switch.Thumb /></Switch.Control></Switch>
|
|
68
|
+
<Dropdown><Button size="sm" variant="ghost" isIconOnly aria-label={`More actions for ${presentation.name}`}><MoreHorizontal size={18} /></Button>
|
|
69
|
+
<Dropdown.Popover placement="bottom end"><Dropdown.Menu aria-label="More actions" onAction={key => { if (key === "uninstall") setConfirm(true); }}>
|
|
70
|
+
<Dropdown.Item id="uninstall" textValue="Uninstall" variant="danger"><Label>Uninstall…</Label></Dropdown.Item>
|
|
71
|
+
</Dropdown.Menu></Dropdown.Popover></Dropdown>
|
|
72
|
+
</>}
|
|
73
|
+
</div>
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
{installed && <ConfirmDialog isOpen={confirm} onOpenChange={setConfirm} title={`Uninstall ${presentation.name}?`} confirmLabel="Uninstall" onConfirm={() => actions.uninstall.mutate(installed, {
|
|
77
|
+
onSuccess: () => notify(`plugin:${plugin.slug}`, "default", `${presentation.name} uninstalled`),
|
|
78
|
+
onError: () => notify(`plugin:${plugin.slug}`, "danger", `${presentation.name} couldn’t be uninstalled`, "Refresh and try again."),
|
|
79
|
+
})}><p>Your saved settings are removed. Works already running keep it. Connectors and connected Providers aren’t affected.</p></ConfirmDialog>}
|
|
80
|
+
{missing.length > 0 && <Alert status="warning"><Alert.Indicator /><Alert.Content>
|
|
81
|
+
<Alert.Title>Version {plugin.version} needs {missing.map(name => parameterLabel(name, plugin.parameters[name]).toLowerCase()).join(", ")}</Alert.Title>
|
|
82
|
+
<Alert.Description>Works you start ask for it first. Add it under Settings to stop being asked.</Alert.Description>
|
|
83
|
+
</Alert.Content></Alert>}
|
|
84
|
+
|
|
85
|
+
<section className="mk-section" aria-labelledby="mk-about">
|
|
86
|
+
<h3 id="mk-about" className="mk-section-title">About</h3>
|
|
87
|
+
<Card className={`mk-group mk-about${presentation.author || presentation.license || presentation.homepage ? "" : " mk-about-single"}`}>
|
|
88
|
+
<div className="mk-about-main">
|
|
89
|
+
<p className="mk-about-text">{plugin.description}</p>
|
|
90
|
+
{presentation.keywords.length > 0 && <p className="mk-keywords">{presentation.keywords.map(word => <Chip key={word} size="sm" variant="soft"><Chip.Label>{word}</Chip.Label></Chip>)}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
{(presentation.author || presentation.license || presentation.homepage) && <dl className="mk-about-facts">
|
|
93
|
+
{presentation.author && <div><dt>Author</dt><dd>{presentation.author}</dd></div>}
|
|
94
|
+
{presentation.license && <div><dt>License</dt><dd>{presentation.license}</dd></div>}
|
|
95
|
+
{presentation.homepage && <div><dt>Homepage</dt><dd><HeroLink href={presentation.homepage} target="_blank" rel="noreferrer">{presentation.homepage.replace(/^https:\/\//, "")}</HeroLink></dd></div>}
|
|
96
|
+
</dl>}
|
|
97
|
+
</Card>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
{plugin.skills.length > 0 && <Group title="Skills" count={plugin.skills.length}>{plugin.skills.map(skill => <li key={skill.name}>
|
|
101
|
+
<CapIcon kind="skill" />
|
|
102
|
+
<span className="mk-row-text"><strong><code>/{skill.name}</code>{skill.argumentHint && <span className="mk-hint"> {skill.argumentHint}</span>}</strong><span>{skill.description}</span></span>
|
|
103
|
+
</li>)}</Group>}
|
|
104
|
+
{plugin.agents.length > 0 && <Group title="Agents" count={plugin.agents.length}>{plugin.agents.map(agent => <li key={agent.name}>
|
|
105
|
+
<CapIcon kind="agent" /><span className="mk-row-text"><strong>{agent.name.split(":").at(-1)}</strong><span>{agent.description}</span></span>
|
|
106
|
+
</li>)}</Group>}
|
|
107
|
+
{plugin.mcpServers.length > 0 && <Group title="Connectors" count={plugin.mcpServers.length}>{plugin.mcpServers.map(server => {
|
|
108
|
+
const connector = server.relationship?.status === "supported" ? server.relationship.connectorName : "";
|
|
109
|
+
// A declaration without a catalog Connector still loads with the Plugin.
|
|
110
|
+
return connector ? <li key={server.name} className="mk-group-link"><Link to={marketplacePaths.connector(connector)}>
|
|
111
|
+
<ConnectorIcon name={connector} size={32} /><span className="mk-row-text"><strong>{serviceLabel(connector)}</strong><span>MCP connector</span></span><ChevronRight size={16} className="mk-muted ml-auto" aria-hidden="true" />
|
|
112
|
+
</Link></li> : <li key={server.name}><CapIcon kind="mcp" /><span className="mk-row-text"><strong>{server.name}</strong><span>Loads with this plugin · not in the Connector catalog</span></span></li>;
|
|
113
|
+
})}</Group>}
|
|
114
|
+
|
|
115
|
+
{!platform && hasSettings && <section className="mk-section">
|
|
116
|
+
<h3 className="mk-section-title">Settings</h3>
|
|
117
|
+
<Card className="mk-group mk-settings-card">
|
|
118
|
+
<form ref={form} noValidate className="mk-form" onSubmit={event => { event.preventDefault(); submit(); }}>
|
|
119
|
+
<ParameterFields declarations={plugin.parameters} drafts={values} errors={errors} redacted={installed?.redactedParameters}
|
|
120
|
+
onChange={(name, value) => { setDrafts(current => ({ ...current, [name]: value })); setErrors(current => ({ ...current, [name]: "" })); }} />
|
|
121
|
+
{installed && <div className="mk-actions"><Button type="submit" variant="secondary" isPending={actions.configure.isPending} isDisabled={!Object.keys(drafts).length}>Save settings</Button></div>}
|
|
122
|
+
</form>
|
|
123
|
+
</Card>
|
|
124
|
+
</section>}
|
|
125
|
+
</div>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function Group({ title, count, children }: { title: string; count: number; children: React.ReactNode }) {
|
|
129
|
+
return <section className="mk-section">
|
|
130
|
+
<h3 className="mk-section-title">{title} <span className="mk-count">{count}</span></h3>
|
|
131
|
+
<Card className="mk-group"><ul className="mk-group-list">{children}</ul></Card>
|
|
132
|
+
</section>;
|
|
133
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Alert, Button, Card, Link as HeroLink, Spinner } from "@heroui/react";
|
|
3
|
+
import type { ProviderAccount } from "@talgent/workbench-contracts/marketplace/public/v1/connections_pb";
|
|
4
|
+
import { Check, ChevronRight, ExternalLink, RefreshCw } from "lucide-react";
|
|
5
|
+
import { Link, useLocation } from "react-router-dom";
|
|
6
|
+
import { connectorStatus, installStatusLabel, useConnectors } from "../../lib/marketplace-catalog";
|
|
7
|
+
import { providerName, serviceLabel, supportedProviders, useConnectProvider, useDisconnectProvider, useProviderAccess, useProviderConnection, useRefreshProviderAccess } from "../../lib/marketplace-connections";
|
|
8
|
+
import { marketplacePaths } from "../../lib/marketplace-routes";
|
|
9
|
+
import { notify } from "../../lib/notifications";
|
|
10
|
+
import { timeAgo } from "../../lib/relative-time";
|
|
11
|
+
import { ConnectorIcon, ProviderIcon } from "./marketplace-icons";
|
|
12
|
+
import { ConfirmDialog, LoadState } from "./marketplace-shared";
|
|
13
|
+
|
|
14
|
+
const descriptions: Record<string, string> = { github: "Repositories, issues and pull requests on GitHub." };
|
|
15
|
+
type Connection = "connected" | "not_connected" | "reconnect_required";
|
|
16
|
+
const connectionOf = (status?: string): Connection => status === "active" ? "connected" : status === "reconnect_required" ? "reconnect_required" : "not_connected";
|
|
17
|
+
const connectionText: Record<Connection, string> = { connected: "Connected", not_connected: "Not connected", reconnect_required: "Authorization expired" };
|
|
18
|
+
|
|
19
|
+
// Every supported Provider with the person's Provider Authorization state; a
|
|
20
|
+
// row opens the Provider page. Profile lists them.
|
|
21
|
+
export function ProviderRows() {
|
|
22
|
+
return <ul className="mk-rows">{supportedProviders.map(provider => <ProviderRow key={provider} provider={provider} />)}</ul>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A row shows the stored status only; who is connected and what the
|
|
26
|
+
// authorization reaches are shown on its page.
|
|
27
|
+
function ProviderRow({ provider }: { provider: string }) {
|
|
28
|
+
const access = useProviderConnection(provider);
|
|
29
|
+
const connection = connectionOf(access.data?.state?.status);
|
|
30
|
+
return <li className="mk-row"><Link className="mk-row-open" to={marketplacePaths.provider(provider)}>
|
|
31
|
+
<ProviderIcon id={provider} /><span className="mk-row-text"><strong>{providerName(provider)}</strong>
|
|
32
|
+
<span className={connection === "reconnect_required" || (access.isError && !access.data) ? "text-warning" : ""}>{access.isPending ? "Checking…" : access.isError && !access.data ? "Connection status unavailable" : connectionText[connection]}</span></span>
|
|
33
|
+
</Link></li>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A Provider page: connecting never installs anything and applies to Works
|
|
37
|
+
// started afterwards. What the authorization reaches is decided on GitHub and
|
|
38
|
+
// shown as last seen; Refresh asks GitHub again.
|
|
39
|
+
export function ProviderPage({ provider }: { provider: string }) {
|
|
40
|
+
const access = useProviderAccess(provider);
|
|
41
|
+
const refresh = useRefreshProviderAccess(provider);
|
|
42
|
+
const connectors = useConnectors();
|
|
43
|
+
const connect = useConnectProvider();
|
|
44
|
+
const disconnect = useDisconnectProvider();
|
|
45
|
+
const { pathname } = useLocation();
|
|
46
|
+
const [confirm, setConfirm] = useState(false);
|
|
47
|
+
const name = providerName(provider);
|
|
48
|
+
if (!access.data) return <LoadState noun={`${name} connection`} isPending={access.isPending} isError={access.isError} hasData={false} isFetching={access.isFetching} onRetry={() => void access.refetch()} />;
|
|
49
|
+
const state = access.data.state;
|
|
50
|
+
const connection = connectionOf(state?.status);
|
|
51
|
+
const observed = access.data.observedAtUnixMs ? new Date(Number(access.data.observedAtUnixMs)) : null;
|
|
52
|
+
const used = (connectors.data ?? []).filter(item => item.provider === provider);
|
|
53
|
+
const affected = used.filter(item => connectorStatus(item) !== "not_installed");
|
|
54
|
+
const start = () => connect.mutate({ provider, returnTo: pathname });
|
|
55
|
+
const confirmDisconnect = () => {
|
|
56
|
+
if (!state?.authorizationId) return;
|
|
57
|
+
disconnect.mutate(state.authorizationId, {
|
|
58
|
+
onSuccess: () => notify(`provider:${provider}`, "default", `${name} disconnected`, "Works you start afterwards can’t use it."),
|
|
59
|
+
onError: () => notify(`provider:${provider}`, "danger", `${name} couldn’t be disconnected`, "GitHub didn’t confirm it. Try again."),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return <div className="mk-detail">
|
|
63
|
+
<header className="mk-detail-head">
|
|
64
|
+
<ProviderIcon id={provider} size={64} />
|
|
65
|
+
<div className="mk-detail-title"><h2>{name}</h2><p className="mk-meta"><span>Provider</span><span className={connection === "reconnect_required" ? "text-warning" : ""}>{connectionText[connection]}</span></p></div>
|
|
66
|
+
<div className="mk-detail-actions">
|
|
67
|
+
{connection === "connected" ? <Button variant="ghost" className="text-danger" isPending={disconnect.isPending} onPress={() => setConfirm(true)}>Disconnect</Button>
|
|
68
|
+
: <Button variant="primary" isPending={connect.isPending} onPress={start}>{connection === "reconnect_required" ? `Reconnect ${name}` : `Connect ${name}`}</Button>}
|
|
69
|
+
</div>
|
|
70
|
+
</header>
|
|
71
|
+
<ConfirmDialog isOpen={confirm} onOpenChange={setConfirm} title={`Disconnect ${name}?`} confirmLabel="Disconnect" onConfirm={confirmDisconnect}>
|
|
72
|
+
<p>Works you start afterwards can’t use {name} until you connect again. Works already running keep their access.</p>
|
|
73
|
+
{affected.length > 0 && <><p className="mt-2">These connectors stay installed but need you to connect again:</p><ul className="mt-1 list-disc pl-5">{affected.map(item => <li key={item.name}>{serviceLabel(item.name)}</li>)}</ul></>}
|
|
74
|
+
</ConfirmDialog>
|
|
75
|
+
{connection === "reconnect_required" && <Alert status="warning"><Alert.Indicator /><Alert.Content><Alert.Title>Your {name} authorization expired</Alert.Title><Alert.Description>New Works start without {name} access until you reconnect. Running Works aren’t changed.</Alert.Description></Alert.Content></Alert>}
|
|
76
|
+
{access.data.accessUnavailable && <Alert status="warning"><Alert.Indicator /><Alert.Content><Alert.Title>{name} couldn’t be checked</Alert.Title><Alert.Description>Showing what it allowed when last seen. Your connection is unchanged.</Alert.Description>
|
|
77
|
+
<Button size="sm" variant="secondary" className="mt-2" isPending={refresh.isPending} onPress={() => refresh.mutate()}>Try again</Button></Alert.Content></Alert>}
|
|
78
|
+
|
|
79
|
+
<section className="mk-section"><h3 className="mk-section-title">About</h3>
|
|
80
|
+
<Card className="mk-group mk-about">
|
|
81
|
+
<div className="mk-about-main"><p className="mk-about-text">{descriptions[provider]} Only you can use this authorization. Works you start after connecting can use it; Works already running keep the access they started with.</p></div>
|
|
82
|
+
<dl className="mk-about-facts">
|
|
83
|
+
<div><dt>Account</dt><dd>{connection === "not_connected" ? "—" : state?.providerLogin}</dd></div>
|
|
84
|
+
<div><dt>Status</dt><dd>{connectionText[connection]}</dd></div>
|
|
85
|
+
</dl>
|
|
86
|
+
</Card>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
{access.data.permissions.length > 0 && <section className="mk-section">
|
|
90
|
+
<h3 className="mk-section-title">{connection === "connected" ? "Permissions" : "Talgent will ask to"}</h3>
|
|
91
|
+
<Card className="mk-group"><ul className="mk-group-list">{access.data.permissions.map(permission => <li key={permission.name}>
|
|
92
|
+
<Check size={16} className="mk-muted" aria-hidden="true" /><span className="mk-row-text"><strong>{serviceLabel(permission.name)}</strong><span>{permission.access === "write" ? "Read and write" : permission.access === "admin" ? "Admin" : "Read only"}</span></span>
|
|
93
|
+
</li>)}</ul></Card>
|
|
94
|
+
</section>}
|
|
95
|
+
|
|
96
|
+
{connection === "connected" && <section className="mk-section">
|
|
97
|
+
<div className="mk-section-bar"><h3 className="mk-section-title">Organizations <span className="mk-count">{access.data.accounts.length}</span></h3>
|
|
98
|
+
<span className="mk-section-actions">
|
|
99
|
+
<span className="mk-note">{observed ? `Checked ${timeAgo(observed)}` : "Not checked yet"}</span>
|
|
100
|
+
<Button size="sm" variant="ghost" isPending={refresh.isPending} onPress={() => refresh.mutate()}>{({ isPending }) => <>{isPending ? <Spinner color="current" size="sm" /> : <RefreshCw size={14} aria-hidden="true" />}Refresh</>}</Button>
|
|
101
|
+
{access.data.manageUrl && <HeroLink href={access.data.manageUrl} target="_blank" rel="noreferrer" className="mk-manage">Manage on {name}<ExternalLink size={13} aria-hidden="true" /></HeroLink>}
|
|
102
|
+
</span></div>
|
|
103
|
+
{access.data.accounts.length ? <Card className="mk-group"><ul className="mk-group-list">{access.data.accounts.map(account => <li key={account.login}>
|
|
104
|
+
<span className="mk-row-text"><strong>{account.login}</strong><span>{scope(account)}</span></span>
|
|
105
|
+
{account.settingsUrl && <HeroLink href={account.settingsUrl} target="_blank" rel="noreferrer" className="mk-manage ml-auto" aria-label={`Change access for ${account.login} on ${name}`}><ExternalLink size={13} /></HeroLink>}
|
|
106
|
+
</li>)}</ul></Card> : <p className="mk-note">{observed ? `${name} doesn’t allow Talgent into any account yet. Add accounts and repositories on ${name}.` : `Refresh to see which accounts ${name} allows.`}</p>}
|
|
107
|
+
<p className="mk-note">Works can check out the repositories these accounts allow. Change which ones on {name}.</p>
|
|
108
|
+
</section>}
|
|
109
|
+
|
|
110
|
+
{used.length > 0 && <section className="mk-section">
|
|
111
|
+
<h3 className="mk-section-title">Connectors that use it <span className="mk-count">{used.length}</span></h3>
|
|
112
|
+
<Card className="mk-group"><ul className="mk-group-list">{used.map(item => <li key={item.name} className="mk-group-link"><Link to={marketplacePaths.connector(item.name)}>
|
|
113
|
+
<ConnectorIcon name={item.name} size={32} /><span className="mk-row-text"><strong>{serviceLabel(item.name)}<span className="mk-kind">Connector</span></strong><span>{installStatusLabel[connectorStatus(item)]}</span></span>
|
|
114
|
+
<ChevronRight size={16} className="mk-muted ml-auto" aria-hidden="true" />
|
|
115
|
+
</Link></li>)}</ul></Card>
|
|
116
|
+
</section>}
|
|
117
|
+
</div>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const scope = (account: ProviderAccount) => `${account.type === "Organization" ? "Organization" : "Personal account"} · ${account.repositorySelection === "all" ? "All repositories" : `${account.repositories.length} selected ${account.repositories.length === 1 ? "repository" : "repositories"}`}`;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { code } from "@streamdown/code";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How text content is shown, decided by the file extension alone:
|
|
5
|
+
* content is never sniffed, so a `.log` holding `<div>` stays plain text.
|
|
6
|
+
*/
|
|
7
|
+
export type TextFormat =
|
|
8
|
+
| { kind: "markdown" }
|
|
9
|
+
| { kind: "html" }
|
|
10
|
+
| { kind: "svg" }
|
|
11
|
+
| { kind: "diff" }
|
|
12
|
+
| { kind: "code"; language: string }
|
|
13
|
+
| { kind: "text" };
|
|
14
|
+
|
|
15
|
+
// Extensions whose Shiki language id differs from the extension itself; any
|
|
16
|
+
// other extension that is a Shiki language id or alias highlights as that.
|
|
17
|
+
const LANGUAGE_BY_EXT: Record<string, string> = {
|
|
18
|
+
ts: "typescript", mts: "typescript", cts: "typescript", js: "javascript", mjs: "javascript", cjs: "javascript",
|
|
19
|
+
py: "python", pyi: "python", rs: "rust", rb: "ruby", kt: "kotlin", kts: "kotlin", cs: "csharp",
|
|
20
|
+
h: "c", hpp: "cpp", cc: "cpp", cxx: "cpp", hh: "cpp", sh: "bash", zsh: "bash", yml: "yaml",
|
|
21
|
+
proto: "proto", tf: "terraform", ex: "elixir", exs: "elixir", erl: "erlang", hs: "haskell",
|
|
22
|
+
ml: "ocaml", fs: "fsharp", pl: "perl", ps1: "powershell", psm1: "powershell", bat: "bat", cmd: "bat",
|
|
23
|
+
sol: "solidity", m: "objective-c", mm: "objective-cpp", vue: "vue", svelte: "svelte", astro: "astro",
|
|
24
|
+
gql: "graphql", jsonc: "jsonc", json5: "json5", ndjson: "json", jsonl: "json", ipynb: "json",
|
|
25
|
+
xml: "xml", xsd: "xml", xsl: "xml", plist: "xml", ini: "ini", cfg: "ini", conf: "ini", env: "dotenv",
|
|
26
|
+
dockerfile: "docker", mk: "make", cmake: "cmake", gradle: "groovy", sbt: "scala", sql: "sql",
|
|
27
|
+
};
|
|
28
|
+
// File names without an extension that still name a language.
|
|
29
|
+
const LANGUAGE_BY_NAME: Record<string, string> = {
|
|
30
|
+
dockerfile: "docker", containerfile: "docker", makefile: "make", gnumakefile: "make",
|
|
31
|
+
jenkinsfile: "groovy", gemfile: "ruby", rakefile: "ruby", ".bashrc": "bash", ".zshrc": "bash",
|
|
32
|
+
};
|
|
33
|
+
// Shiki knows these, but they read as the text they are.
|
|
34
|
+
const PLAIN_EXTENSIONS = new Set(["txt", "text", "log", "csv", "tsv", "out", "err"]);
|
|
35
|
+
|
|
36
|
+
export function fileName(path?: string): string {
|
|
37
|
+
return path?.split(/[?#]/, 1)[0]?.split("/").pop() ?? "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function fileExtension(path?: string): string | undefined {
|
|
41
|
+
return fileName(path).match(/.\.([^.]+)$/)?.[1]?.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The Shiki language for a path, or undefined when it is not code. */
|
|
45
|
+
export function codeLanguage(path?: string): string | undefined {
|
|
46
|
+
const name = fileName(path).toLowerCase();
|
|
47
|
+
const ext = fileExtension(path);
|
|
48
|
+
if (!ext) return LANGUAGE_BY_NAME[name];
|
|
49
|
+
if (PLAIN_EXTENSIONS.has(ext)) return undefined;
|
|
50
|
+
const language = LANGUAGE_BY_EXT[ext] ?? ext;
|
|
51
|
+
return code.supportsLanguage(language as Parameters<typeof code.supportsLanguage>[0]) ? language : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function textFormat(path?: string): TextFormat {
|
|
55
|
+
switch (fileExtension(path)) {
|
|
56
|
+
case "md": case "markdown": return { kind: "markdown" };
|
|
57
|
+
case "html": case "htm": return { kind: "html" };
|
|
58
|
+
case "svg": return { kind: "svg" };
|
|
59
|
+
case "diff": case "patch": return { kind: "diff" };
|
|
60
|
+
}
|
|
61
|
+
const language = codeLanguage(path);
|
|
62
|
+
return language ? { kind: "code", language } : { kind: "text" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The Shiki language a format's source reads in, for source views. */
|
|
66
|
+
export function sourceLanguage(format: TextFormat): string {
|
|
67
|
+
switch (format.kind) {
|
|
68
|
+
case "markdown": return "markdown";
|
|
69
|
+
case "html": return "html";
|
|
70
|
+
case "svg": return "xml";
|
|
71
|
+
case "diff": return "diff";
|
|
72
|
+
case "code": return format.language;
|
|
73
|
+
case "text": return "text";
|
|
74
|
+
}
|
|
75
|
+
}
|