@yancyyu/openhermit 1.6.29 → 1.6.30
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-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DsQt4FHy.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-CrWocIjq.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-B6d8ysWi.js} +1 -1
- package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-DAIYCFP8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
- package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DZou1667.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
- package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-DY3qbzqj.js} +1 -1
- package/dist-renderer/assets/{index-BowUl0Jb.js → index-BlOrAXp3.js} +542 -532
- package/dist-renderer/assets/{index-6m1ZAymG.js → index-Bs27J5gB.js} +1 -1
- package/dist-renderer/assets/{index-Dp3kJTEe.js → index-C8B_nKOF.js} +1 -1
- package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
- package/dist-renderer/assets/{index-TOpt_T7A.js → index-DLKyDr4T.js} +1 -1
- package/dist-renderer/assets/{index-qNBNjW4K.js → index-Dhsk3_DD.js} +1 -1
- package/dist-renderer/assets/{index-vAykq1H1.js → index-GpUvV2xs.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
- package/dist-renderer/assets/{layout-DNANbrI4.js → layout-BZLlNmbr.js} +1 -1
- package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-qz6v45xy.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-CGKpOUF2.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +209 -6
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/api/httpClient.ts +61 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +35 -0
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
- package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/teamSlice.ts +8 -2
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
- package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
- package/dist-renderer/assets/index-BhellmRb.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExtensionToast — lightweight notification system for Extension Store events.
|
|
3
|
+
* Shows success/error/warning/info toasts with secret masking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { AlertTriangle, CheckCircle, Info, X } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
11
|
+
|
|
12
|
+
export interface ToastMessage {
|
|
13
|
+
id: string;
|
|
14
|
+
type: ToastType;
|
|
15
|
+
title: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExtensionToastProps {
|
|
20
|
+
toasts: ToastMessage[];
|
|
21
|
+
onDismiss: (id: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TOAST_STYLES: Record<
|
|
25
|
+
ToastType,
|
|
26
|
+
{ bg: string; border: string; icon: typeof CheckCircle; iconColor: string }
|
|
27
|
+
> = {
|
|
28
|
+
success: {
|
|
29
|
+
bg: 'bg-emerald-500/10',
|
|
30
|
+
border: 'border-emerald-500/30',
|
|
31
|
+
icon: CheckCircle,
|
|
32
|
+
iconColor: 'text-emerald-400',
|
|
33
|
+
},
|
|
34
|
+
error: {
|
|
35
|
+
bg: 'bg-red-500/10',
|
|
36
|
+
border: 'border-red-500/30',
|
|
37
|
+
icon: AlertTriangle,
|
|
38
|
+
iconColor: 'text-red-400',
|
|
39
|
+
},
|
|
40
|
+
warning: {
|
|
41
|
+
bg: 'bg-amber-500/10',
|
|
42
|
+
border: 'border-amber-500/30',
|
|
43
|
+
icon: AlertTriangle,
|
|
44
|
+
iconColor: 'text-amber-400',
|
|
45
|
+
},
|
|
46
|
+
info: {
|
|
47
|
+
bg: 'bg-blue-500/10',
|
|
48
|
+
border: 'border-blue-500/30',
|
|
49
|
+
icon: Info,
|
|
50
|
+
iconColor: 'text-blue-400',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const AUTO_HIDE_MS: Record<ToastType, number> = {
|
|
55
|
+
success: 3000,
|
|
56
|
+
error: 0,
|
|
57
|
+
warning: 5000,
|
|
58
|
+
info: 4000,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function maskSecrets(text: string): string {
|
|
62
|
+
return text.replace(
|
|
63
|
+
/(?:token|secret|key|password|api[_-]?key|auth|credential)["\s:=]+(["']?)([\w\-./+=]{8,})\1/gi,
|
|
64
|
+
'$1[REDACTED]$1'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ToastItem({ toast, onDismiss }: { toast: ToastMessage; onDismiss: () => void }) {
|
|
69
|
+
const style = TOAST_STYLES[toast.type];
|
|
70
|
+
const Icon = style.icon;
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const ms = AUTO_HIDE_MS[toast.type];
|
|
74
|
+
if (ms > 0) {
|
|
75
|
+
const timer = setTimeout(onDismiss, ms);
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}, [toast.type, onDismiss]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={`flex items-start gap-2 rounded-md border px-3 py-2 shadow-lg ${style.bg} ${style.border}`}
|
|
83
|
+
>
|
|
84
|
+
<Icon className={`mt-0.5 size-4 shrink-0 ${style.iconColor}`} />
|
|
85
|
+
<div className="min-w-0 flex-1">
|
|
86
|
+
<p className="text-sm font-medium text-text">{toast.title}</p>
|
|
87
|
+
{toast.message && (
|
|
88
|
+
<p className="mt-0.5 text-xs text-text-muted">{maskSecrets(toast.message)}</p>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
<button onClick={onDismiss} className="shrink-0 text-text-muted hover:text-text">
|
|
92
|
+
<X className="size-3.5" />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const ExtensionToast = ({ toasts, onDismiss }: ExtensionToastProps): React.JSX.Element => {
|
|
99
|
+
if (toasts.length === 0) return <></>;
|
|
100
|
+
return (
|
|
101
|
+
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" style={{ maxWidth: 360 }}>
|
|
102
|
+
{toasts.map((toast) => (
|
|
103
|
+
<ToastItem key={toast.id} toast={toast} onDismiss={() => onDismiss(toast.id)} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let toastSeq = 0;
|
|
110
|
+
|
|
111
|
+
export function useExtensionToast() {
|
|
112
|
+
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
113
|
+
|
|
114
|
+
const addToast = useCallback((type: ToastType, title: string, message?: string) => {
|
|
115
|
+
const id = `ext-toast-${++toastSeq}`;
|
|
116
|
+
setToasts((prev) => [...prev, { id, type, title, message }]);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const dismissToast = useCallback((id: string) => {
|
|
120
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
return { toasts, addToast, dismissToast };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* StoreExtensionToast — reads toasts from Zustand store (set by install/uninstall/diagnostics).
|
|
128
|
+
* Drop into ExtensionStoreView to show toast notifications.
|
|
129
|
+
*/
|
|
130
|
+
import { useStore } from '@renderer/store';
|
|
131
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
132
|
+
|
|
133
|
+
export const StoreExtensionToast = (): React.JSX.Element => {
|
|
134
|
+
const { toasts, dismissToast } = useStore(
|
|
135
|
+
useShallow((s) => ({
|
|
136
|
+
toasts: s.extensionToasts,
|
|
137
|
+
dismissToast: s.dismissExtensionToast,
|
|
138
|
+
}))
|
|
139
|
+
);
|
|
140
|
+
return <ExtensionToast toasts={toasts} onDismiss={dismissToast} />;
|
|
141
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarnessSelector — dropdown for choosing which runtime harness to install into.
|
|
3
|
+
* Filters available harnesses by capability (mcp / skills).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Label } from '@renderer/components/ui/label';
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from '@renderer/components/ui/select';
|
|
14
|
+
|
|
15
|
+
type HarnessCapability = 'mcp' | 'skills';
|
|
16
|
+
|
|
17
|
+
interface HarnessOption {
|
|
18
|
+
value: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HARNESS_OPTIONS: Record<HarnessCapability, HarnessOption[]> = {
|
|
23
|
+
// MCP install is only correct for Claude Code and Cursor today. Codex/Gemini/OpenCode
|
|
24
|
+
// adapters delegate to Claude's CLI and would write the wrong config, so they are not
|
|
25
|
+
// offered here to avoid silently misrouting installs.
|
|
26
|
+
mcp: [
|
|
27
|
+
{ value: 'claudecode', label: 'Claude Code' },
|
|
28
|
+
{ value: 'cursor', label: 'Cursor' },
|
|
29
|
+
],
|
|
30
|
+
skills: [
|
|
31
|
+
{ value: 'claudecode', label: 'Claude Code' },
|
|
32
|
+
{ value: 'codex', label: 'Codex' },
|
|
33
|
+
{ value: 'gemini', label: 'Gemini CLI' },
|
|
34
|
+
{ value: 'opencode', label: 'OpenCode' },
|
|
35
|
+
{ value: 'cursor', label: 'Cursor' },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface HarnessSelectorProps {
|
|
40
|
+
capability: HarnessCapability;
|
|
41
|
+
value: string;
|
|
42
|
+
onChange: (value: string) => void;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const HarnessSelector = ({
|
|
47
|
+
capability,
|
|
48
|
+
value,
|
|
49
|
+
onChange,
|
|
50
|
+
disabled,
|
|
51
|
+
}: HarnessSelectorProps): React.JSX.Element => {
|
|
52
|
+
const options = HARNESS_OPTIONS[capability];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex items-center gap-3">
|
|
56
|
+
<Label className="shrink-0 text-xs text-text-muted">运行时</Label>
|
|
57
|
+
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
|
58
|
+
<SelectTrigger className="h-8 w-40 text-xs">
|
|
59
|
+
<SelectValue placeholder="选择运行时" />
|
|
60
|
+
</SelectTrigger>
|
|
61
|
+
<SelectContent>
|
|
62
|
+
{options.map((opt) => (
|
|
63
|
+
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
64
|
+
{opt.label}
|
|
65
|
+
</SelectItem>
|
|
66
|
+
))}
|
|
67
|
+
</SelectContent>
|
|
68
|
+
</Select>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnvVarPanel — manage global and project-level environment variables.
|
|
3
|
+
* Auto-scans skills for required-env declarations, shows what needs to be filled.
|
|
4
|
+
* Variables are injected into agent sessions via resolveAgentEnv.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { api } from '@renderer/api';
|
|
10
|
+
import { Button } from '@renderer/components/ui/button';
|
|
11
|
+
import { Input } from '@renderer/components/ui/input';
|
|
12
|
+
import { Label } from '@renderer/components/ui/label';
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from '@renderer/components/ui/select';
|
|
20
|
+
import {
|
|
21
|
+
AlertTriangle,
|
|
22
|
+
CheckCircle2,
|
|
23
|
+
Eye,
|
|
24
|
+
EyeOff,
|
|
25
|
+
Plus,
|
|
26
|
+
RefreshCw,
|
|
27
|
+
Trash2,
|
|
28
|
+
XCircle,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
|
|
31
|
+
import type { SkillEnvVarDef } from '@shared/types/extensions';
|
|
32
|
+
|
|
33
|
+
type Scope = 'global' | 'project';
|
|
34
|
+
|
|
35
|
+
interface EnvVarSource {
|
|
36
|
+
name: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
isRequired: boolean;
|
|
39
|
+
from: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EnvEntry {
|
|
43
|
+
key: string;
|
|
44
|
+
value: string;
|
|
45
|
+
source?: EnvVarSource;
|
|
46
|
+
isNew?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const EnvVarPanel = ({ projectPath }: { projectPath: string | null }): React.JSX.Element => {
|
|
50
|
+
const [scope, setScope] = useState<Scope>('global');
|
|
51
|
+
const [entries, setEntries] = useState<EnvEntry[]>([]);
|
|
52
|
+
const [loading, setLoading] = useState(true);
|
|
53
|
+
const [saving, setSaving] = useState(false);
|
|
54
|
+
const [showValues, setShowValues] = useState<Record<number, boolean>>({});
|
|
55
|
+
const [dirty, setDirty] = useState(false);
|
|
56
|
+
|
|
57
|
+
const loadEntries = useCallback(async () => {
|
|
58
|
+
setLoading(true);
|
|
59
|
+
try {
|
|
60
|
+
// Load saved values
|
|
61
|
+
const rawSaved: Record<string, any> =
|
|
62
|
+
scope === 'global'
|
|
63
|
+
? api.credentials
|
|
64
|
+
? await api.credentials.getSkillGlobalEnv('__global__')
|
|
65
|
+
: {}
|
|
66
|
+
: projectPath && api.credentials
|
|
67
|
+
? await api.credentials.getProjectEnv(projectPath)
|
|
68
|
+
: {};
|
|
69
|
+
const saved: Record<string, string> = {};
|
|
70
|
+
for (const [k, v] of Object.entries(rawSaved)) {
|
|
71
|
+
if (typeof v === 'string') saved[k] = v;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scan skills for required-env declarations
|
|
75
|
+
const sourceMap = new Map<string, EnvVarSource>();
|
|
76
|
+
if (api.skills) {
|
|
77
|
+
try {
|
|
78
|
+
const skills = await api.skills.list(projectPath ?? undefined);
|
|
79
|
+
for (const skill of skills) {
|
|
80
|
+
if (!skill.requiredEnv?.length) continue;
|
|
81
|
+
for (const v of skill.requiredEnv) {
|
|
82
|
+
const existing = sourceMap.get(v.name);
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.from.push(skill.name);
|
|
85
|
+
if (v.isRequired !== false) existing.isRequired = true;
|
|
86
|
+
} else {
|
|
87
|
+
sourceMap.set(v.name, {
|
|
88
|
+
name: v.name,
|
|
89
|
+
description: v.description,
|
|
90
|
+
isRequired: v.isRequired !== false,
|
|
91
|
+
from: [skill.name],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
/* non-critical */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Merge: sources + saved values + custom entries
|
|
102
|
+
const allKeys = new Set([...Object.keys(saved), ...sourceMap.keys()]);
|
|
103
|
+
const parsed: EnvEntry[] = [];
|
|
104
|
+
|
|
105
|
+
// First: required vars from skills (with source info)
|
|
106
|
+
for (const [, src] of sourceMap) {
|
|
107
|
+
parsed.push({
|
|
108
|
+
key: src.name,
|
|
109
|
+
value: saved[src.name] ?? '',
|
|
110
|
+
source: src,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Then: custom saved vars not declared by any skill
|
|
115
|
+
for (const key of Object.keys(saved)) {
|
|
116
|
+
if (!sourceMap.has(key)) {
|
|
117
|
+
parsed.push({ key, value: saved[key] });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setEntries(parsed);
|
|
122
|
+
} catch {
|
|
123
|
+
setEntries([]);
|
|
124
|
+
} finally {
|
|
125
|
+
setLoading(false);
|
|
126
|
+
setDirty(false);
|
|
127
|
+
}
|
|
128
|
+
}, [scope, projectPath]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
void loadEntries();
|
|
132
|
+
}, [loadEntries]);
|
|
133
|
+
|
|
134
|
+
const addEntry = () => {
|
|
135
|
+
setEntries((prev) => [...prev, { key: '', value: '', isNew: true }]);
|
|
136
|
+
setDirty(true);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const removeEntry = (i: number) => {
|
|
140
|
+
setEntries((prev) => prev.filter((_, idx) => idx !== i));
|
|
141
|
+
setDirty(true);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const updateEntry = (i: number, field: 'key' | 'value', val: string) => {
|
|
145
|
+
setEntries((prev) => prev.map((e, idx) => (idx === i ? { ...e, [field]: val } : e)));
|
|
146
|
+
setDirty(true);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const toggleShow = (i: number) => {
|
|
150
|
+
setShowValues((prev) => ({ ...prev, [i]: !prev[i] }));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleSave = async () => {
|
|
154
|
+
const vars: Record<string, string> = {};
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const key = entry.key.trim();
|
|
157
|
+
if (key) {
|
|
158
|
+
vars[key] = entry.value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setSaving(true);
|
|
163
|
+
try {
|
|
164
|
+
if (scope === 'global') {
|
|
165
|
+
await api.credentials?.saveSkillGlobalEnv('__global__', vars);
|
|
166
|
+
} else if (projectPath) {
|
|
167
|
+
await api.credentials?.saveProjectEnv(projectPath, vars);
|
|
168
|
+
}
|
|
169
|
+
await loadEntries();
|
|
170
|
+
} catch {
|
|
171
|
+
// Silently fail
|
|
172
|
+
} finally {
|
|
173
|
+
setSaving(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const requiredEntries = entries.filter((e) => e.source?.isRequired);
|
|
178
|
+
const missingRequired = requiredEntries.filter((e) => !e.value.trim());
|
|
179
|
+
const filledCount = entries.filter((e) => e.key.trim() && e.value.trim()).length;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="space-y-4">
|
|
183
|
+
<div className="flex items-center justify-between">
|
|
184
|
+
<div className="space-y-1">
|
|
185
|
+
<Label className="text-sm">环境变量</Label>
|
|
186
|
+
<p className="text-xs text-text-muted">
|
|
187
|
+
配置运行时环境变量,启动 agent 时自动注入。
|
|
188
|
+
{scope === 'global' ? '全局变量所有项目生效。' : '项目变量仅当前项目生效并可覆盖全局。'}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<Button
|
|
193
|
+
variant="ghost"
|
|
194
|
+
size="icon"
|
|
195
|
+
className="size-7"
|
|
196
|
+
onClick={() => void loadEntries()}
|
|
197
|
+
disabled={loading}
|
|
198
|
+
>
|
|
199
|
+
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
|
|
200
|
+
</Button>
|
|
201
|
+
<Select value={scope} onValueChange={(v) => setScope(v as Scope)}>
|
|
202
|
+
<SelectTrigger className="h-8 w-28 text-xs">
|
|
203
|
+
<SelectValue />
|
|
204
|
+
</SelectTrigger>
|
|
205
|
+
<SelectContent>
|
|
206
|
+
<SelectItem value="global">全局</SelectItem>
|
|
207
|
+
<SelectItem value="project" disabled={!projectPath}>
|
|
208
|
+
{projectPath ? '当前项目' : '无项目'}
|
|
209
|
+
</SelectItem>
|
|
210
|
+
</SelectContent>
|
|
211
|
+
</Select>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{loading ? (
|
|
216
|
+
<div className="py-8 text-center text-xs text-text-muted">扫描环境变量需求...</div>
|
|
217
|
+
) : (
|
|
218
|
+
<>
|
|
219
|
+
{/* Missing required banner */}
|
|
220
|
+
{missingRequired.length > 0 && (
|
|
221
|
+
<div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-400">
|
|
222
|
+
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
|
223
|
+
<span>缺少必填变量:{missingRequired.map((e) => e.key).join(', ')}</span>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Entries */}
|
|
228
|
+
<div className="space-y-3">
|
|
229
|
+
{entries.map((entry, i) => (
|
|
230
|
+
<div key={i} className="space-y-1">
|
|
231
|
+
<div className="flex items-center gap-2">
|
|
232
|
+
<Input
|
|
233
|
+
value={entry.key}
|
|
234
|
+
onChange={(e) => updateEntry(i, 'key', e.target.value)}
|
|
235
|
+
className="h-8 w-48 font-mono text-xs"
|
|
236
|
+
placeholder="ENV_VAR_NAME"
|
|
237
|
+
disabled={!!entry.source}
|
|
238
|
+
/>
|
|
239
|
+
<div className="relative flex-1">
|
|
240
|
+
<Input
|
|
241
|
+
type={showValues[i] ? 'text' : 'password'}
|
|
242
|
+
value={entry.value}
|
|
243
|
+
onChange={(e) => updateEntry(i, 'value', e.target.value)}
|
|
244
|
+
className="h-8 pr-8 text-xs"
|
|
245
|
+
placeholder={entry.source?.isRequired ? '必填' : '可选'}
|
|
246
|
+
/>
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => toggleShow(i)}
|
|
250
|
+
className="hover:text-text-default absolute right-2 top-1/2 -translate-y-1/2 text-text-muted"
|
|
251
|
+
>
|
|
252
|
+
{showValues[i] ? (
|
|
253
|
+
<EyeOff className="size-3.5" />
|
|
254
|
+
) : (
|
|
255
|
+
<Eye className="size-3.5" />
|
|
256
|
+
)}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
<Button
|
|
260
|
+
variant="ghost"
|
|
261
|
+
size="icon"
|
|
262
|
+
className="size-8 shrink-0 text-red-400 hover:bg-red-500/10"
|
|
263
|
+
onClick={() => removeEntry(i)}
|
|
264
|
+
>
|
|
265
|
+
<Trash2 className="size-3.5" />
|
|
266
|
+
</Button>
|
|
267
|
+
</div>
|
|
268
|
+
{/* Source info */}
|
|
269
|
+
{entry.source && (
|
|
270
|
+
<div className="flex items-center gap-2 pl-1 text-[11px] text-text-muted">
|
|
271
|
+
<span
|
|
272
|
+
className={`rounded px-1 py-0.5 font-medium ${
|
|
273
|
+
entry.source.isRequired
|
|
274
|
+
? entry.value.trim()
|
|
275
|
+
? 'bg-green-500/10 text-green-500'
|
|
276
|
+
: 'bg-red-500/10 text-red-400'
|
|
277
|
+
: 'bg-surface-raised text-text-muted'
|
|
278
|
+
}`}
|
|
279
|
+
>
|
|
280
|
+
{entry.source.isRequired ? (entry.value.trim() ? '已配置' : '必填') : '可选'}
|
|
281
|
+
</span>
|
|
282
|
+
<span>来自 {entry.source.from.join(', ')}</span>
|
|
283
|
+
{entry.source.description && <span>— {entry.source.description}</span>}
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Add button */}
|
|
291
|
+
<Button variant="outline" size="sm" onClick={addEntry} className="h-8 gap-1.5 text-xs">
|
|
292
|
+
<Plus className="size-3.5" />
|
|
293
|
+
添加变量
|
|
294
|
+
</Button>
|
|
295
|
+
|
|
296
|
+
{/* Status bar */}
|
|
297
|
+
{entries.length > 0 && (
|
|
298
|
+
<div className="flex items-center gap-3 text-xs text-text-muted">
|
|
299
|
+
<span className="flex items-center gap-1">
|
|
300
|
+
<CheckCircle2 className="size-3 text-green-500" />
|
|
301
|
+
{filledCount} 已配置
|
|
302
|
+
</span>
|
|
303
|
+
{missingRequired.length > 0 && (
|
|
304
|
+
<span className="flex items-center gap-1">
|
|
305
|
+
<XCircle className="size-3 text-red-400" />
|
|
306
|
+
{missingRequired.length} 缺失
|
|
307
|
+
</span>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{entries.length === 0 && (
|
|
313
|
+
<div className="py-6 text-center text-xs text-text-muted">
|
|
314
|
+
未检测到需要的环境变量。安装的 Skills 如果声明了所需变量,会自动出现在这里。
|
|
315
|
+
<br />
|
|
316
|
+
也可以手动添加。
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{/* Save */}
|
|
321
|
+
{dirty && (
|
|
322
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
323
|
+
<Button variant="ghost" size="sm" onClick={loadEntries}>
|
|
324
|
+
取消
|
|
325
|
+
</Button>
|
|
326
|
+
<Button size="sm" onClick={() => void handleSave()} disabled={saving}>
|
|
327
|
+
{saving ? '保存中...' : '保存'}
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
};
|