-
Notifications
You must be signed in to change notification settings - Fork 888
feat(host-service): host agent configs (v2 PR 1, argv-array shape) #3914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d61bd8d
feat(host-service): host agent configs (v2 PR 1, argv-array shape)
Kitenite 3eb6ca4
test(host-service): run agent-configs router tests against real migra…
Kitenite 6f23b95
feat(desktop): polish V2 agents settings UI to match v1 (icons, drag-…
Kitenite 669a745
feat(desktop): show preset icons in Add agent dropdown
Kitenite 0cbe6d7
feat(host-service): add Mastracode preset (--prompt, no suffix)
Kitenite 1e012ba
fix(host-service): address review tier 1 + tier 2 on agent configs
Kitenite File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
10 changes: 10 additions & 0 deletions
10
...nderer/routes/_authenticated/settings/agents/components/AgentsSettings/AgentsSettings.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
263 changes: 263 additions & 0 deletions
263
...er/routes/_authenticated/settings/agents/components/V2AgentsSettings/V2AgentsSettings.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| import { | ||
| closestCenter, | ||
| DndContext, | ||
| type DragEndEvent, | ||
| KeyboardSensor, | ||
| MouseSensor, | ||
| TouchSensor, | ||
| useSensor, | ||
| useSensors, | ||
| } from "@dnd-kit/core"; | ||
| import { | ||
| arrayMove, | ||
| SortableContext, | ||
| sortableKeyboardCoordinates, | ||
| verticalListSortingStrategy, | ||
| } from "@dnd-kit/sortable"; | ||
| import type { | ||
| AgentPreset, | ||
| HostAgentConfigDto, | ||
| } from "@superset/host-service/settings"; | ||
| import { Button } from "@superset/ui/button"; | ||
| import { | ||
| DropdownMenu, | ||
| DropdownMenuContent, | ||
| DropdownMenuItem, | ||
| DropdownMenuTrigger, | ||
| } from "@superset/ui/dropdown-menu"; | ||
| import { toast } from "@superset/ui/sonner"; | ||
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
| import { Plus, RotateCcw } from "lucide-react"; | ||
| import { useMemo } from "react"; | ||
| import { | ||
| getPresetIcon, | ||
| useIsDarkTheme, | ||
| } from "renderer/assets/app-icons/preset-icons"; | ||
| import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; | ||
| import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; | ||
| import { V2AgentCard } from "./components/V2AgentCard"; | ||
|
|
||
| const QUERY_KEY = ["host-agent-configs"] as const; | ||
|
|
||
| export function V2AgentsSettings() { | ||
| const { activeHostUrl } = useLocalHostService(); | ||
| const queryClient = useQueryClient(); | ||
| const isDark = useIsDarkTheme(); | ||
|
|
||
| const configsQuery = useQuery({ | ||
| queryKey: [...QUERY_KEY, activeHostUrl] as const, | ||
| enabled: !!activeHostUrl, | ||
| queryFn: () => { | ||
| if (!activeHostUrl) return [] as HostAgentConfigDto[]; | ||
| return getHostServiceClientByUrl( | ||
| activeHostUrl, | ||
| ).settings.agentConfigs.list.query(); | ||
| }, | ||
| }); | ||
|
|
||
| const presetsQuery = useQuery({ | ||
| queryKey: [...QUERY_KEY, "presets", activeHostUrl] as const, | ||
| enabled: !!activeHostUrl, | ||
| queryFn: () => { | ||
| if (!activeHostUrl) return [] as AgentPreset[]; | ||
| return getHostServiceClientByUrl( | ||
| activeHostUrl, | ||
| ).settings.agentConfigs.listPresets.query(); | ||
| }, | ||
| }); | ||
|
|
||
| const invalidate = () => | ||
| queryClient.invalidateQueries({ queryKey: [...QUERY_KEY, activeHostUrl] }); | ||
|
|
||
| const addMutation = useMutation({ | ||
| mutationFn: (presetId: string) => { | ||
| if (!activeHostUrl) throw new Error("Host service is not available"); | ||
| return getHostServiceClientByUrl( | ||
| activeHostUrl, | ||
| ).settings.agentConfigs.add.mutate({ presetId }); | ||
| }, | ||
| onSuccess: () => invalidate(), | ||
| onError: (err) => | ||
| toast.error(err instanceof Error ? err.message : "Failed to add agent"), | ||
| }); | ||
|
|
||
| const reorderMutation = useMutation({ | ||
| mutationFn: (ids: string[]) => { | ||
| if (!activeHostUrl) throw new Error("Host service is not available"); | ||
| return getHostServiceClientByUrl( | ||
| activeHostUrl, | ||
| ).settings.agentConfigs.reorder.mutate({ ids }); | ||
| }, | ||
| onMutate: async (ids) => { | ||
| await queryClient.cancelQueries({ | ||
| queryKey: [...QUERY_KEY, activeHostUrl], | ||
| }); | ||
| const previous = queryClient.getQueryData<HostAgentConfigDto[]>([ | ||
| ...QUERY_KEY, | ||
| activeHostUrl, | ||
| ]); | ||
| if (previous) { | ||
| const byId = new Map(previous.map((row) => [row.id, row])); | ||
| const next = ids | ||
| .map((id, index) => { | ||
| const row = byId.get(id); | ||
| return row ? { ...row, order: index } : null; | ||
| }) | ||
| .filter((row): row is HostAgentConfigDto => row !== null); | ||
| queryClient.setQueryData([...QUERY_KEY, activeHostUrl], next); | ||
| } | ||
| return { previous }; | ||
| }, | ||
| onError: (err, _ids, ctx) => { | ||
| if (ctx?.previous) { | ||
| queryClient.setQueryData([...QUERY_KEY, activeHostUrl], ctx.previous); | ||
| } | ||
| toast.error(err instanceof Error ? err.message : "Failed to reorder"); | ||
| }, | ||
| onSettled: () => invalidate(), | ||
| }); | ||
|
|
||
| const resetMutation = useMutation({ | ||
| mutationFn: () => { | ||
| if (!activeHostUrl) throw new Error("Host service is not available"); | ||
| return getHostServiceClientByUrl( | ||
| activeHostUrl, | ||
| ).settings.agentConfigs.resetToDefaults.mutate(); | ||
| }, | ||
| onSuccess: () => invalidate(), | ||
| onError: (err) => | ||
| toast.error(err instanceof Error ? err.message : "Failed to reset"), | ||
| }); | ||
|
|
||
| const sensors = useSensors( | ||
| useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), | ||
| useSensor(TouchSensor, { | ||
| activationConstraint: { delay: 150, tolerance: 5 }, | ||
| }), | ||
| useSensor(KeyboardSensor, { | ||
| coordinateGetter: sortableKeyboardCoordinates, | ||
| }), | ||
| ); | ||
|
|
||
| const configs = configsQuery.data ?? []; | ||
| const presets = presetsQuery.data ?? []; | ||
| const sortableIds = useMemo(() => configs.map((row) => row.id), [configs]); | ||
| const descriptionByPresetId = useMemo( | ||
| () => | ||
| new Map(presets.map((preset) => [preset.presetId, preset.description])), | ||
| [presets], | ||
| ); | ||
|
|
||
| const handleDragEnd = (event: DragEndEvent) => { | ||
| const { active, over } = event; | ||
| if (!over || active.id === over.id) return; | ||
| const oldIndex = sortableIds.indexOf(String(active.id)); | ||
| const newIndex = sortableIds.indexOf(String(over.id)); | ||
| if (oldIndex < 0 || newIndex < 0) return; | ||
| reorderMutation.mutate(arrayMove(sortableIds, oldIndex, newIndex)); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="p-6 max-w-5xl w-full"> | ||
| <div className="mb-8 flex items-start justify-between gap-4"> | ||
| <div> | ||
| <h2 className="text-xl font-semibold">Agents</h2> | ||
| <p className="text-sm text-muted-foreground mt-1"> | ||
| Configure terminal agents available on this host. Drag to reorder. | ||
| </p> | ||
| </div> | ||
| <div className="flex items-center gap-2 shrink-0"> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => resetMutation.mutate()} | ||
| disabled={resetMutation.isPending} | ||
| > | ||
| <RotateCcw className="size-4" /> Reset to defaults | ||
| </Button> | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button size="sm"> | ||
| <Plus className="size-4" /> Add agent | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent align="end"> | ||
| {presets.map((preset) => { | ||
| const icon = getPresetIcon(preset.presetId, isDark); | ||
| return ( | ||
| <DropdownMenuItem | ||
| key={preset.presetId} | ||
| onSelect={() => addMutation.mutate(preset.presetId)} | ||
| className="gap-2" | ||
| > | ||
| {icon ? ( | ||
| <img | ||
| src={icon} | ||
| alt="" | ||
| className="size-4 object-contain shrink-0" | ||
| /> | ||
| ) : ( | ||
| <div className="size-4 rounded bg-muted shrink-0" /> | ||
| )} | ||
| {preset.label} | ||
| </DropdownMenuItem> | ||
| ); | ||
| })} | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| </div> | ||
| </div> | ||
|
|
||
| {configsQuery.isLoading ? ( | ||
| <p className="text-sm text-muted-foreground"> | ||
| Loading agent settings... | ||
| </p> | ||
| ) : configsQuery.isError ? ( | ||
| <div className="space-y-2"> | ||
| <p className="text-sm text-destructive"> | ||
| Couldn't load agent settings:{" "} | ||
| {configsQuery.error instanceof Error | ||
| ? configsQuery.error.message | ||
| : "host service unavailable"} | ||
| </p> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => configsQuery.refetch()} | ||
| > | ||
| Retry | ||
| </Button> | ||
| </div> | ||
| ) : configs.length === 0 ? ( | ||
| <p className="text-sm text-muted-foreground"> | ||
| No agents configured. Add one from the menu above. | ||
| </p> | ||
| ) : ( | ||
| <DndContext | ||
| sensors={sensors} | ||
| collisionDetection={closestCenter} | ||
| onDragEnd={handleDragEnd} | ||
| > | ||
| <SortableContext | ||
| items={sortableIds} | ||
| strategy={verticalListSortingStrategy} | ||
| > | ||
| <div className="space-y-3"> | ||
| {configs.map((config) => ( | ||
| <V2AgentCard | ||
| key={config.id} | ||
| config={config} | ||
| description={ | ||
| descriptionByPresetId.get(config.presetId) ?? | ||
| "Terminal agent launch configuration" | ||
| } | ||
| onChanged={invalidate} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </SortableContext> | ||
| </DndContext> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent overlapping reorder writes.
reorderpersists the entire ordered id list, so firing a second mutation before the first settles can let an older request win and save a stale order. Please serialize these mutations or ignore new drag-end events whilereorderMutation.isPending.Suggested minimal guard
const handleDragEnd = (event: DragEndEvent) => { + if (reorderMutation.isPending) return; const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = sortableIds.indexOf(String(active.id)); const newIndex = sortableIds.indexOf(String(over.id)); if (oldIndex < 0 || newIndex < 0) return; reorderMutation.mutate(arrayMove(sortableIds, oldIndex, newIndex)); };📝 Committable suggestion
🤖 Prompt for AI Agents