apteva 0.2.11 → 0.3.7

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.
@@ -4,7 +4,8 @@ import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, Re
4
4
  import { Select } from "../common/Select";
5
5
  import { useConfirm } from "../common/Modal";
6
6
  import { useAuth } from "../../context";
7
- import type { Agent, Provider, AgentFeatures, McpServer } from "../../types";
7
+ import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, AgentMode, MultiAgentConfig } from "../../types";
8
+ import { getMultiAgentConfig } from "../../types";
8
9
 
9
10
  type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
10
11
 
@@ -885,25 +886,41 @@ function FilesTab({ agent }: { agent: Agent }) {
885
886
  );
886
887
  }
887
888
 
889
+ interface AvailableSkill {
890
+ id: string;
891
+ name: string;
892
+ description: string;
893
+ version: string;
894
+ enabled: boolean;
895
+ project_id: string | null;
896
+ }
897
+
888
898
  function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
889
899
  agent: Agent;
890
900
  providers: Provider[];
891
901
  onUpdateAgent: (updates: Partial<Agent>) => Promise<{ error?: string }>;
892
902
  onDeleteAgent: () => void;
893
903
  }) {
894
- const { authFetch } = useAuth();
904
+ const { authFetch, isDev } = useAuth();
895
905
  const [form, setForm] = useState({
896
906
  name: agent.name,
897
907
  provider: agent.provider,
898
908
  model: agent.model,
899
909
  systemPrompt: agent.systemPrompt,
900
- features: { ...agent.features },
910
+ features: {
911
+ ...agent.features,
912
+ builtinTools: agent.features.builtinTools || { webSearch: false, webFetch: false },
913
+ },
901
914
  mcpServers: [...(agent.mcpServers || [])],
915
+ skills: [...(agent.skills || [])],
902
916
  });
903
917
  const [saving, setSaving] = useState(false);
904
918
  const [confirmDelete, setConfirmDelete] = useState(false);
905
919
  const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
906
920
  const [availableMcpServers, setAvailableMcpServers] = useState<McpServer[]>([]);
921
+ const [availableSkills, setAvailableSkills] = useState<AvailableSkill[]>([]);
922
+ const [apiKey, setApiKey] = useState<string | null>(null);
923
+ const [showApiKey, setShowApiKey] = useState(false);
907
924
 
908
925
  // Fetch available MCP servers
909
926
  useEffect(() => {
@@ -919,6 +936,37 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
919
936
  fetchMcpServers();
920
937
  }, [authFetch]);
921
938
 
939
+ // Fetch API key (dev mode only)
940
+ useEffect(() => {
941
+ if (!isDev) return;
942
+ const fetchApiKey = async () => {
943
+ try {
944
+ const res = await authFetch(`/api/agents/${agent.id}/api-key`);
945
+ if (res.ok) {
946
+ const data = await res.json();
947
+ setApiKey(data.apiKey);
948
+ }
949
+ } catch (e) {
950
+ // Ignore - not critical
951
+ }
952
+ };
953
+ fetchApiKey();
954
+ }, [agent.id, isDev, authFetch]);
955
+
956
+ // Fetch available skills
957
+ useEffect(() => {
958
+ const fetchSkills = async () => {
959
+ try {
960
+ const res = await authFetch("/api/skills");
961
+ const data = await res.json();
962
+ setAvailableSkills(data.skills || []);
963
+ } catch (e) {
964
+ console.error("Failed to fetch skills:", e);
965
+ }
966
+ };
967
+ fetchSkills();
968
+ }, [authFetch]);
969
+
922
970
  // Reset form when agent changes
923
971
  useEffect(() => {
924
972
  setForm({
@@ -926,8 +974,12 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
926
974
  provider: agent.provider,
927
975
  model: agent.model,
928
976
  systemPrompt: agent.systemPrompt,
929
- features: { ...agent.features },
977
+ features: {
978
+ ...agent.features,
979
+ builtinTools: agent.features.builtinTools || { webSearch: false, webFetch: false },
980
+ },
930
981
  mcpServers: [...(agent.mcpServers || [])],
982
+ skills: [...(agent.skills || [])],
931
983
  });
932
984
  setMessage(null);
933
985
  }, [agent.id]);
@@ -951,10 +1003,63 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
951
1003
  };
952
1004
 
953
1005
  const toggleFeature = (key: keyof AgentFeatures) => {
954
- setForm(prev => ({
955
- ...prev,
956
- features: { ...prev.features, [key]: !prev.features[key] },
957
- }));
1006
+ if (key === "agents") {
1007
+ // Special handling for agents feature - convert to MultiAgentConfig
1008
+ const current = prev => {
1009
+ const agentConfig = getMultiAgentConfig(prev.features, agent.projectId);
1010
+ return agentConfig.enabled;
1011
+ };
1012
+ setForm(prev => {
1013
+ const isEnabled = typeof prev.features.agents === "boolean"
1014
+ ? prev.features.agents
1015
+ : (prev.features.agents as MultiAgentConfig)?.enabled ?? false;
1016
+ if (isEnabled) {
1017
+ // Turning off - set to false
1018
+ return { ...prev, features: { ...prev.features, agents: false } };
1019
+ } else {
1020
+ // Turning on - set to config with defaults
1021
+ return {
1022
+ ...prev,
1023
+ features: {
1024
+ ...prev.features,
1025
+ agents: { enabled: true, mode: "worker" as AgentMode, group: agent.projectId || undefined },
1026
+ },
1027
+ };
1028
+ }
1029
+ });
1030
+ } else {
1031
+ setForm(prev => ({
1032
+ ...prev,
1033
+ features: { ...prev.features, [key]: !prev.features[key] },
1034
+ }));
1035
+ }
1036
+ };
1037
+
1038
+ // Set multi-agent mode
1039
+ const setAgentMode = (mode: AgentMode) => {
1040
+ setForm(prev => {
1041
+ const currentConfig = getMultiAgentConfig(prev.features, agent.projectId);
1042
+ return {
1043
+ ...prev,
1044
+ features: {
1045
+ ...prev.features,
1046
+ agents: { ...currentConfig, enabled: true, mode },
1047
+ },
1048
+ };
1049
+ });
1050
+ };
1051
+
1052
+ // Helper to check if agents feature is enabled
1053
+ const isAgentsEnabled = () => {
1054
+ const agentsVal = form.features.agents;
1055
+ if (typeof agentsVal === "boolean") return agentsVal;
1056
+ return (agentsVal as MultiAgentConfig)?.enabled ?? false;
1057
+ };
1058
+
1059
+ // Get current agent mode
1060
+ const getAgentMode = (): AgentMode => {
1061
+ const config = getMultiAgentConfig(form.features, agent.projectId);
1062
+ return config.mode || "worker";
958
1063
  };
959
1064
 
960
1065
  const toggleMcpServer = (serverId: string) => {
@@ -966,6 +1071,15 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
966
1071
  }));
967
1072
  };
968
1073
 
1074
+ const toggleSkill = (skillId: string) => {
1075
+ setForm(prev => ({
1076
+ ...prev,
1077
+ skills: prev.skills.includes(skillId)
1078
+ ? prev.skills.filter(id => id !== skillId)
1079
+ : [...prev.skills, skillId],
1080
+ }));
1081
+ };
1082
+
969
1083
  const handleSave = async () => {
970
1084
  setSaving(true);
971
1085
  setMessage(null);
@@ -985,7 +1099,8 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
985
1099
  form.model !== agent.model ||
986
1100
  form.systemPrompt !== agent.systemPrompt ||
987
1101
  JSON.stringify(form.features) !== JSON.stringify(agent.features) ||
988
- JSON.stringify(form.mcpServers.sort()) !== JSON.stringify((agent.mcpServers || []).sort());
1102
+ JSON.stringify(form.mcpServers.sort()) !== JSON.stringify((agent.mcpServers || []).sort()) ||
1103
+ JSON.stringify(form.skills.sort()) !== JSON.stringify((agent.skills || []).sort());
989
1104
 
990
1105
  return (
991
1106
  <div className="flex-1 overflow-auto p-4">
@@ -1025,28 +1140,130 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1025
1140
 
1026
1141
  <FormField label="Features">
1027
1142
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
1028
- {FEATURE_CONFIG.map(({ key, label, description, icon: Icon }) => (
1143
+ {FEATURE_CONFIG.map(({ key, label, description, icon: Icon }) => {
1144
+ // For agents feature, check the enabled property of the config
1145
+ const isEnabled = key === "agents" ? isAgentsEnabled() : !!form.features[key];
1146
+ return (
1147
+ <button
1148
+ key={key}
1149
+ type="button"
1150
+ onClick={() => toggleFeature(key)}
1151
+ className={`flex items-center gap-3 p-3 rounded border text-left transition ${
1152
+ isEnabled
1153
+ ? "border-[#f97316] bg-[#f97316]/10"
1154
+ : "border-[#222] hover:border-[#333]"
1155
+ }`}
1156
+ >
1157
+ <Icon className={`w-5 h-5 flex-shrink-0 ${isEnabled ? "text-[#f97316]" : "text-[#666]"}`} />
1158
+ <div className="flex-1 min-w-0">
1159
+ <div className={`text-sm font-medium ${isEnabled ? "text-[#f97316]" : ""}`}>
1160
+ {label}
1161
+ </div>
1162
+ <div className="text-xs text-[#666]">{description}</div>
1163
+ </div>
1164
+ </button>
1165
+ );
1166
+ })}
1167
+ </div>
1168
+ </FormField>
1169
+
1170
+ {/* Multi-Agent Mode Selection - shown when agents is enabled */}
1171
+ {isAgentsEnabled() && (
1172
+ <FormField label="Multi-Agent Mode">
1173
+ <div className="flex gap-2">
1029
1174
  <button
1030
- key={key}
1031
1175
  type="button"
1032
- onClick={() => toggleFeature(key)}
1033
- className={`flex items-center gap-3 p-3 rounded border text-left transition ${
1034
- form.features[key]
1176
+ onClick={() => setAgentMode("coordinator")}
1177
+ className={`flex-1 p-3 rounded border text-left transition ${
1178
+ getAgentMode() === "coordinator"
1035
1179
  ? "border-[#f97316] bg-[#f97316]/10"
1036
1180
  : "border-[#222] hover:border-[#333]"
1037
1181
  }`}
1038
1182
  >
1039
- <Icon className={`w-5 h-5 flex-shrink-0 ${form.features[key] ? "text-[#f97316]" : "text-[#666]"}`} />
1040
- <div className="flex-1 min-w-0">
1041
- <div className={`text-sm font-medium ${form.features[key] ? "text-[#f97316]" : ""}`}>
1042
- {label}
1043
- </div>
1044
- <div className="text-xs text-[#666]">{description}</div>
1183
+ <div className={`text-sm font-medium ${getAgentMode() === "coordinator" ? "text-[#f97316]" : ""}`}>
1184
+ Coordinator
1185
+ </div>
1186
+ <div className="text-xs text-[#666]">Orchestrates and delegates to other agents</div>
1187
+ </button>
1188
+ <button
1189
+ type="button"
1190
+ onClick={() => setAgentMode("worker")}
1191
+ className={`flex-1 p-3 rounded border text-left transition ${
1192
+ getAgentMode() === "worker"
1193
+ ? "border-[#f97316] bg-[#f97316]/10"
1194
+ : "border-[#222] hover:border-[#333]"
1195
+ }`}
1196
+ >
1197
+ <div className={`text-sm font-medium ${getAgentMode() === "worker" ? "text-[#f97316]" : ""}`}>
1198
+ Worker
1045
1199
  </div>
1200
+ <div className="text-xs text-[#666]">Receives tasks from coordinators</div>
1046
1201
  </button>
1047
- ))}
1202
+ </div>
1203
+ {agent.projectId && (
1204
+ <p className="text-xs text-[#555] mt-2">
1205
+ Group: Using project as agent group
1206
+ </p>
1207
+ )}
1208
+ </FormField>
1209
+ )}
1210
+
1211
+ {/* Agent Built-in Tools - Anthropic only */}
1212
+ {form.provider === "anthropic" && (
1213
+ <FormField label="Agent Built-in Tools">
1214
+ <div className="flex flex-wrap gap-2">
1215
+ <button
1216
+ type="button"
1217
+ onClick={() => setForm(prev => ({
1218
+ ...prev,
1219
+ features: {
1220
+ ...prev.features,
1221
+ builtinTools: {
1222
+ ...prev.features.builtinTools,
1223
+ webSearch: !prev.features.builtinTools?.webSearch,
1224
+ },
1225
+ },
1226
+ }))}
1227
+ className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
1228
+ form.features.builtinTools?.webSearch
1229
+ ? "border-[#f97316] bg-[#f97316]/10 text-[#f97316]"
1230
+ : "border-[#222] hover:border-[#333] text-[#888]"
1231
+ }`}
1232
+ >
1233
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1234
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
1235
+ </svg>
1236
+ <span className="text-sm">Web Search</span>
1237
+ </button>
1238
+ <button
1239
+ type="button"
1240
+ onClick={() => setForm(prev => ({
1241
+ ...prev,
1242
+ features: {
1243
+ ...prev.features,
1244
+ builtinTools: {
1245
+ ...prev.features.builtinTools,
1246
+ webFetch: !prev.features.builtinTools?.webFetch,
1247
+ },
1248
+ },
1249
+ }))}
1250
+ className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
1251
+ form.features.builtinTools?.webFetch
1252
+ ? "border-[#f97316] bg-[#f97316]/10 text-[#f97316]"
1253
+ : "border-[#222] hover:border-[#333] text-[#888]"
1254
+ }`}
1255
+ >
1256
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1257
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
1258
+ </svg>
1259
+ <span className="text-sm">Web Fetch</span>
1260
+ </button>
1048
1261
  </div>
1262
+ <p className="text-xs text-[#555] mt-2">
1263
+ Provider-native tools for real-time web access
1264
+ </p>
1049
1265
  </FormField>
1266
+ )}
1050
1267
 
1051
1268
  {/* MCP Server Selection - shown when MCP is enabled */}
1052
1269
  {form.features.mcp && (
@@ -1057,7 +1274,9 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1057
1274
  </p>
1058
1275
  ) : (
1059
1276
  <div className="space-y-2">
1060
- {availableMcpServers.map(server => {
1277
+ {availableMcpServers
1278
+ .filter(server => server.project_id === null || server.project_id === agent.projectId)
1279
+ .map(server => {
1061
1280
  const isRemote = server.type === "http" && server.url;
1062
1281
  const isAvailable = isRemote || server.status === "running";
1063
1282
  const serverInfo = isRemote
@@ -1078,8 +1297,13 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1078
1297
  isAvailable ? "bg-green-400" : "bg-[#444]"
1079
1298
  }`} />
1080
1299
  <div className="flex-1 min-w-0">
1081
- <div className={`text-sm font-medium ${form.mcpServers.includes(server.id) ? "text-[#f97316]" : ""}`}>
1082
- {server.name}
1300
+ <div className="flex items-center gap-2">
1301
+ <span className={`text-sm font-medium ${form.mcpServers.includes(server.id) ? "text-[#f97316]" : ""}`}>
1302
+ {server.name}
1303
+ </span>
1304
+ {server.project_id === null && (
1305
+ <span className="text-[10px] text-[#666] bg-[#1a1a1a] px-1.5 py-0.5 rounded">Global</span>
1306
+ )}
1083
1307
  </div>
1084
1308
  <div className="text-xs text-[#666]">{serverInfo}</div>
1085
1309
  </div>
@@ -1101,6 +1325,50 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1101
1325
  </FormField>
1102
1326
  )}
1103
1327
 
1328
+ {/* Skills Selection */}
1329
+ <FormField label="Skills">
1330
+ {availableSkills.length === 0 ? (
1331
+ <p className="text-sm text-[#666]">
1332
+ No skills configured. Add skills in the Skills page first.
1333
+ </p>
1334
+ ) : (
1335
+ <div className="space-y-2">
1336
+ {availableSkills
1337
+ .filter(s => s.enabled && (s.project_id === null || s.project_id === agent.projectId))
1338
+ .map(skill => (
1339
+ <button
1340
+ key={skill.id}
1341
+ type="button"
1342
+ onClick={() => toggleSkill(skill.id)}
1343
+ className={`w-full flex items-center gap-3 p-3 rounded border text-left transition ${
1344
+ form.skills.includes(skill.id)
1345
+ ? "border-[#f97316] bg-[#f97316]/10"
1346
+ : "border-[#222] hover:border-[#333]"
1347
+ }`}
1348
+ >
1349
+ <div className="flex-1 min-w-0">
1350
+ <div className="flex items-center gap-2">
1351
+ <span className={`text-sm font-medium ${form.skills.includes(skill.id) ? "text-[#f97316]" : ""}`}>
1352
+ {skill.name}
1353
+ </span>
1354
+ {skill.project_id === null && (
1355
+ <span className="text-[10px] text-[#666] bg-[#1a1a1a] px-1.5 py-0.5 rounded">Global</span>
1356
+ )}
1357
+ </div>
1358
+ <div className="text-xs text-[#666]">{skill.description}</div>
1359
+ </div>
1360
+ <div className="text-xs px-2 py-0.5 rounded bg-[#222] text-[#666]">
1361
+ v{skill.version}
1362
+ </div>
1363
+ </button>
1364
+ ))}
1365
+ <p className="text-xs text-[#666] mt-2">
1366
+ Skills provide reusable instructions for the agent.
1367
+ </p>
1368
+ </div>
1369
+ )}
1370
+ </FormField>
1371
+
1104
1372
  {message && (
1105
1373
  <div className={`text-sm px-3 py-2 rounded ${
1106
1374
  message.type === "success"
@@ -1125,6 +1393,47 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1125
1393
  </p>
1126
1394
  )}
1127
1395
 
1396
+ {/* Developer Info (dev mode only) */}
1397
+ {isDev && apiKey && (
1398
+ <div className="mt-8 pt-6 border-t border-[#222]">
1399
+ <p className="text-sm text-[#666] mb-3">Developer Info</p>
1400
+ <div className="space-y-2">
1401
+ <div className="flex items-center justify-between">
1402
+ <span className="text-xs text-[#666]">Agent ID</span>
1403
+ <code className="text-xs bg-[#1a1a1a] px-2 py-1 rounded text-[#888]">{agent.id}</code>
1404
+ </div>
1405
+ <div className="flex items-center justify-between">
1406
+ <span className="text-xs text-[#666]">Port</span>
1407
+ <code className="text-xs bg-[#1a1a1a] px-2 py-1 rounded text-[#888]">{agent.port || "N/A"}</code>
1408
+ </div>
1409
+ <div className="flex flex-col gap-1">
1410
+ <div className="flex items-center justify-between">
1411
+ <span className="text-xs text-[#666]">API Key</span>
1412
+ <button
1413
+ onClick={() => setShowApiKey(!showApiKey)}
1414
+ className="text-xs text-[#f97316] hover:text-[#fb923c]"
1415
+ >
1416
+ {showApiKey ? "Hide" : "Show"}
1417
+ </button>
1418
+ </div>
1419
+ {showApiKey && (
1420
+ <code className="text-xs bg-[#1a1a1a] px-2 py-1 rounded text-[#888] break-all">
1421
+ {apiKey}
1422
+ </code>
1423
+ )}
1424
+ </div>
1425
+ {agent.status === "running" && agent.port && (
1426
+ <div className="flex flex-col gap-1 mt-2">
1427
+ <span className="text-xs text-[#666]">Test with curl</span>
1428
+ <code className="text-xs bg-[#1a1a1a] px-2 py-1.5 rounded text-[#666] break-all">
1429
+ curl -H "X-API-Key: {showApiKey ? apiKey : "***"}" http://localhost:{agent.port}/config
1430
+ </code>
1431
+ </div>
1432
+ )}
1433
+ </div>
1434
+ </div>
1435
+ )}
1436
+
1128
1437
  {/* Danger Zone */}
1129
1438
  <div className="mt-8 pt-6 border-t border-[#222]">
1130
1439
  <p className="text-sm text-[#666] mb-3">Danger Zone</p>
@@ -90,7 +90,7 @@ export function AgentsView({
90
90
  ) : filteredAgents.length === 0 ? (
91
91
  <EmptyState onNewAgent={onNewAgent} canCreateAgent={canCreateAgent} hasProjectFilter={currentProjectId !== null} />
92
92
  ) : (
93
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
93
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3 auto-rows-fr">
94
94
  {filteredAgents.map((agent) => (
95
95
  <AgentCard
96
96
  key={agent.id}