Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
import { formatDateTimeInTimezone } from "@superset/shared/rrule";
import { cn } from "@superset/ui/utils";
import { useMutation } from "@tanstack/react-query";
import { useEnabledAgents } from "renderer/hooks/useEnabledAgents";
import { apiTrpcClient } from "renderer/lib/api-trpc-client";
import { DevicePicker } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker";
import { useWorkspaceHostOptions } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions";
Expand All @@ -29,7 +28,6 @@ export function AutomationDetailSidebar({
automation,
recentRuns,
}: AutomationDetailSidebarProps) {
const { agents: enabledAgents } = useEnabledAgents();
const recentProjects = useRecentProjects();
const { localHostId } = useWorkspaceHostOptions();
const selectedProject = recentProjects.find(
Expand Down Expand Up @@ -149,10 +147,20 @@ export function AutomationDetailSidebar({
value={
<AgentPicker
className="-mr-4"
value={automation.agentConfig.id}
hostId={hostId}
value={automation.agent}
onChange={(id) => {
const config = enabledAgents.find((a) => a.id === id);
if (config) updateMutation.mutate({ agentConfig: config });
// The picker is scoped to `hostId`; if the automation
// was previously auto-routed (targetHostId null), pin it
// to the host this id came from so a UUID-shaped agent
// can't be dispatched to a host that's never seen it.
const patch: { agent: string; targetHostId?: string } = {
agent: id,
};
if (!automation.targetHostId && hostId) {
patch.targetHostId = hostId;
}
updateMutation.mutate(patch);
}}
/>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { getPresetById } from "@superset/shared/host-agent-presets";
import { LuCpu } from "react-icons/lu";
import { usePresetIcon } from "renderer/assets/app-icons/preset-icons";
import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl";
import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices";

export function AgentCell({
agentId,
label,
hostId,
}: {
agentId: string;
label: string;
hostId: string | null;
}) {
const icon = usePresetIcon(agentId);
const hostUrl = useHostUrl(hostId);
const { agents } = useV2AgentChoices(hostUrl);
const hostMatch = agents.find((option) => option.id === agentId);
const presetMatch = hostMatch ? null : getPresetById(agentId);
const label = hostMatch?.label ?? presetMatch?.label ?? agentId;
const iconKey = hostMatch?.iconId ?? presetMatch?.presetId ?? agentId;
const icon = usePresetIcon(iconKey);

return (
<span className="inline-flex items-center gap-1.5">
{icon ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getPresetById } from "@superset/shared/host-agent-presets";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -9,25 +10,36 @@ import { getPresetIcon } from "@superset/ui/icons/preset-icons";
import { useNavigate } from "@tanstack/react-router";
import { HiCheck } from "react-icons/hi2";
import { LuCpu, LuSettings } from "react-icons/lu";
import {
useIsDarkTheme,
usePresetIcon,
} from "renderer/assets/app-icons/preset-icons";
import { useIsDarkTheme } from "renderer/assets/app-icons/preset-icons";
import { PickerTrigger } from "renderer/components/PickerTrigger";
import { useEnabledAgents } from "renderer/hooks/useEnabledAgents";
import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl";
import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices";

interface AgentPickerProps {
hostId: string | null | undefined;
value: string;
onChange: (next: string) => void;
className?: string;
}

export function AgentPicker({ value, onChange, className }: AgentPickerProps) {
export function AgentPicker({
hostId,
value,
onChange,
className,
}: AgentPickerProps) {
const navigate = useNavigate();
const { agents } = useEnabledAgents();
const hostUrl = useHostUrl(hostId);
const { agents } = useV2AgentChoices(hostUrl);
const isDark = useIsDarkTheme();
const selectedAgent = agents.find((agent) => agent.id === value);
const selectedIcon = usePresetIcon(value);
const hostMatch = agents.find((agent) => agent.id === value);
const presetMatch = hostMatch ? null : getPresetById(value);
const selectedLabel =
hostMatch?.label ?? presetMatch?.label ?? (value ? value : null);
const selectedIconKey = hostMatch?.iconId ?? presetMatch?.presetId ?? value;
const selectedIcon = selectedIconKey
? getPresetIcon(selectedIconKey, isDark)
: null;

return (
<DropdownMenu>
Expand All @@ -45,12 +57,12 @@ export function AgentPicker({ value, onChange, className }: AgentPickerProps) {
<LuCpu className="size-4 shrink-0" />
)
}
label={selectedAgent?.label ?? "Select agent"}
label={selectedLabel ?? "Select agent"}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
{agents.map((agent) => {
const icon = getPresetIcon(agent.id, isDark);
const icon = getPresetIcon(agent.iconId ?? agent.id, isDark);
return (
<DropdownMenuItem
key={agent.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
} from "@superset/ui/dialog";
import { toast } from "@superset/ui/sonner";
import { useMutation } from "@tanstack/react-query";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { LuX } from "react-icons/lu";
import { EmojiTextInput } from "renderer/components/EmojiTextInput";
import { MarkdownEditor } from "renderer/components/MarkdownEditor";
import { useEnabledAgents } from "renderer/hooks/useEnabledAgents";
import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl";
import { useV2AgentChoices } from "renderer/hooks/useV2AgentChoices";
import { apiTrpcClient } from "renderer/lib/api-trpc-client";
import { DevicePicker } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker";
import { useWorkspaceHostOptions } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions";
Expand Down Expand Up @@ -54,23 +55,29 @@ export function CreateAutomationDialog({
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
null,
);
const [agentType, setAgentType] = useState("claude");
const [agent, setAgent] = useState<string | null>(null);
const [rrule, setRrule] = useState(DEFAULT_RRULE);
const [v2WorkspaceId, setV2WorkspaceId] = useState<string | null>(null);

const { localHostId } = useWorkspaceHostOptions();
const targetHostId = hostId ?? localHostId;
const hostUrl = useHostUrl(targetHostId);
const { agents: hostAgents } = useV2AgentChoices(hostUrl);
const recentProjects = useRecentProjects();
const { agents: enabledAgents } = useEnabledAgents();
const searchFiles = useProjectFileSearch({
hostId,
projectId: selectedProjectId,
});
const selectedProject = recentProjects.find(
(project) => project.id === selectedProjectId,
);
const selectedAgentConfig = enabledAgents.find(
(agent) => agent.id === agentType,
);
const selectedAgent = hostAgents.find((option) => option.id === agent);

useEffect(() => {
if (agent && hostAgents.some((option) => option.id === agent)) return;
const fallback = hostAgents[0]?.id ?? null;
if (fallback !== agent) setAgent(fallback);
}, [agent, hostAgents]);
Comment on lines +76 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Agent selection cleared during transient hostAgents reload

When the user switches the target host (via DevicePicker), hostAgents briefly becomes [] while the new host's agents are fetched. During that window the guard hostAgents.some((option) => option.id === agent) fails (empty list), fallback evaluates to null, and setAgent(null) fires — clearing the in-progress selection. The picker then shows empty and the Create button becomes disabled until the load settles. Using a stable "previous value" pattern (e.g. keeping the old agent until the new list loads and then applying the fallback only if the list is non-empty) would avoid the flash.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/CreateAutomationDialog/CreateAutomationDialog.tsx
Line: 76-80

Comment:
**Agent selection cleared during transient `hostAgents` reload**

When the user switches the target host (via `DevicePicker`), `hostAgents` briefly becomes `[]` while the new host's agents are fetched. During that window the guard `hostAgents.some((option) => option.id === agent)` fails (empty list), `fallback` evaluates to `null`, and `setAgent(null)` fires — clearing the in-progress selection. The picker then shows empty and the Create button becomes disabled until the load settles. Using a stable "previous value" pattern (e.g. keeping the old agent until the new list loads and then applying the fallback only if the list is non-empty) would avoid the flash.

How can I resolve this? If you propose a fix, please make it concise.


// Default to first project once the Electric-synced list lands.
useEffect(() => {
Expand All @@ -80,43 +87,67 @@ export function CreateAutomationDialog({
if (first) setSelectedProjectId(first.id);
}, [open, selectedProjectId, recentProjects]);

// Track which (open session, template) we've already pre-filled so the
// effects don't re-run and stomp on user edits when `hostAgents` lands
// asynchronously.
const appliedTemplateRef = useRef<AutomationTemplate | null>(null);
const appliedAgentForTemplateRef = useRef<AutomationTemplate | null>(null);

const applyTemplate = useCallback((template: AutomationTemplate) => {
setName(template.name);
setPrompt(template.prompt);
if (template.agentType) setAgentType(template.agentType);
if (template.rrule) setRrule(template.rrule);
}, []);
Comment on lines 96 to 100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Template selection from the gallery no longer applies the template’s preferred agent, causing automations to run with the wrong agent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/CreateAutomationDialog/CreateAutomationDialog.tsx, line 96:

<comment>Template selection from the gallery no longer applies the template’s preferred agent, causing automations to run with the wrong agent.</comment>

<file context>
@@ -87,30 +87,44 @@ export function CreateAutomationDialog({
+	const appliedTemplateRef = useRef<AutomationTemplate | null>(null);
+	const appliedAgentForTemplateRef = useRef<AutomationTemplate | null>(null);
+
+	const applyTemplate = useCallback((template: AutomationTemplate) => {
+		setName(template.name);
+		setPrompt(template.prompt);
</file context>
Suggested change
const applyTemplate = useCallback((template: AutomationTemplate) => {
setName(template.name);
setPrompt(template.prompt);
if (template.agentType) setAgentType(template.agentType);
if (template.rrule) setRrule(template.rrule);
}, []);
const applyTemplate = useCallback(
(template: AutomationTemplate) => {
setName(template.name);
setPrompt(template.prompt);
if (template.agentType) {
const match = hostAgents.find(
(option) =>
option.id === template.agentType ||
option.iconId === template.agentType,
);
if (match) setAgent(match.id);
}
if (template.rrule) setRrule(template.rrule);
},
[hostAgents],
);

Tip: Review your code locally with the cubic CLI to iterate faster.


// Pre-fill when opened with an initialTemplate (from the empty-state gallery).
// Pre-fill scalar fields once when opened with an initialTemplate.
useEffect(() => {
if (!open) return;
if (!initialTemplate) return;
if (appliedTemplateRef.current === initialTemplate) return;
appliedTemplateRef.current = initialTemplate;
applyTemplate(initialTemplate);
}, [open, initialTemplate, applyTemplate]);

// Match the template's preferred agent against the host's choices once
// they load. Separate effect so a `hostAgents` refresh doesn't re-trigger
// the scalar prefill above.
useEffect(() => {
if (!open) return;
if (!initialTemplate?.agentType) return;
if (appliedAgentForTemplateRef.current === initialTemplate) return;
if (hostAgents.length === 0) return;
const match = hostAgents.find(
(option) =>
option.id === initialTemplate.agentType ||
option.iconId === initialTemplate.agentType,
);
if (match) setAgent(match.id);
appliedAgentForTemplateRef.current = initialTemplate;
}, [open, initialTemplate, hostAgents]);

useEffect(() => {
if (!open) {
setView("compose");
setName("");
setPrompt("");
setHostId(null);
setSelectedProjectId(null);
setAgentType("claude");
setAgent(null);
setRrule(DEFAULT_RRULE);
setV2WorkspaceId(null);
appliedTemplateRef.current = null;
appliedAgentForTemplateRef.current = null;
}
}, [open]);

const targetHostId = hostId ?? localHostId;

const createMutation = useMutation({
mutationFn: () => {
if (!selectedAgentConfig) throw new Error("No agent selected");
if (!selectedAgent) throw new Error("No agent selected");
if (!selectedProjectId) throw new Error("No project selected");
return apiTrpcClient.automation.create.mutate({
name,
prompt,
agentConfig: selectedAgentConfig,
agent: selectedAgent.id,
targetHostId: targetHostId ?? null,
v2ProjectId: selectedProjectId,
v2WorkspaceId,
Expand Down Expand Up @@ -149,7 +180,7 @@ export function CreateAutomationDialog({
prompt.trim().length > 0 &&
!!selectedProjectId &&
!!targetHostId &&
!!selectedAgentConfig &&
!!selectedAgent &&
rrule.trim().length > 0 &&
!createMutation.isPending;

Expand Down Expand Up @@ -258,8 +289,9 @@ export function CreateAutomationDialog({
/>
<AgentPicker
className="w-[100px]"
value={agentType}
onChange={setAgentType}
hostId={targetHostId}
value={agent ?? ""}
onChange={setAgent}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,8 @@ function AutomationsPage() {

<span className="min-w-0 text-xs text-muted-foreground">
<AgentCell
agentId={automation.agentConfig.id}
label={automation.agentConfig.label}
agentId={automation.agent}
Comment on lines 481 to +484
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Agent label missing for automations without a pinned host

AgentCell receives hostId={automation.targetHostId ?? null}, but automations created before this PR (and CLI-created ones) typically have targetHostId = null. With a null hostId, useHostUrl returns no URL, useV2AgentChoices returns [], match is undefined, and the cell falls back to rendering the raw agentId. For backfilled slug values ("claude", "codex") this is readable but unstyled; for any UUID-format agent IDs it shows the raw UUID. The AutomationDetailSidebar handles this correctly by falling back to localHostId — the same pattern should be applied here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx
Line: 481-484

Comment:
**Agent label missing for automations without a pinned host**

`AgentCell` receives `hostId={automation.targetHostId ?? null}`, but automations created before this PR (and CLI-created ones) typically have `targetHostId = null`. With a null `hostId`, `useHostUrl` returns no URL, `useV2AgentChoices` returns `[]`, `match` is undefined, and the cell falls back to rendering the raw `agentId`. For backfilled slug values ("claude", "codex") this is readable but unstyled; for any UUID-format agent IDs it shows the raw UUID. The `AutomationDetailSidebar` handles this correctly by falling back to `localHostId` — the same pattern should be applied here.

How can I resolve this? If you propose a fix, please make it concise.

hostId={automation.targetHostId ?? null}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Auto-targeted automations are resolved against the viewer’s local host when rendering AgentCell, which can show incorrect agent labels/icons compared to the actual dispatch host.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx, line 485:

<comment>Auto-targeted automations are resolved against the viewer’s local host when rendering `AgentCell`, which can show incorrect agent labels/icons compared to the actual dispatch host.</comment>

<file context>
@@ -481,8 +481,8 @@ function AutomationsPage() {
-												agentId={automation.agentConfig.id}
-												label={automation.agentConfig.label}
+												agentId={automation.agent}
+												hostId={automation.targetHostId ?? null}
 											/>
 										</span>
</file context>

/>
</span>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export function HistoryDropdown() {
.select(({ automations }) => ({
id: automations.id,
name: automations.name,
agentId: automations.agentConfig.id,
agentId: automations.agent,
})),
[collections],
);
Expand Down
6 changes: 2 additions & 4 deletions apps/docs/content/docs/cli/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -849,8 +849,7 @@ Use [`automations logs`](#superset-automations-logs-id) for run history.
{ flag: "--workspace <workspaceId>", description: "Reuse an existing workspace." },
{ flag: "--project <projectId>", description: "Project ID for new-workspace-per-run mode." },
{ flag: "--host <hostId>", description: "Default: owner's online host." },
{ flag: "--agent <presetId>", description: "Default: claude." },
{ flag: "--agent-config-file <path>", description: "Full ResolvedAgentConfig JSON; overrides --agent." },
{ flag: "--agent <agent>", description: "Host agent presetId, instance UUID, or 'superset' for built-in chat. Default: claude." },
]}
output="Automation"
>
Expand Down Expand Up @@ -881,8 +880,7 @@ superset automations create \
{ flag: "--host <hostId>", description: "New target host." },
{ flag: "--project <projectId>", description: "New v2 project ID." },
{ flag: "--workspace <workspaceId>", description: "New v2 workspace ID." },
{ flag: "--agent <presetId>", description: "New agent preset." },
{ flag: "--agent-config-file <path>", description: "Overrides --agent when both provided." },
{ flag: "--agent <agent>", description: "New host agent presetId, instance UUID, or 'superset'." },
{ flag: "--mcp-scope <a,b,c>", description: "Comma-separated MCP scope strings." },
{ flag: "--enabled / --no-enabled", description: "Calls automation.setEnabled first." },
]}
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/sdk/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ Create a recurring automation.
const a = await client.automations.create({
name: 'Daily leads',
prompt: 'Find new leads from Linear and update the CRM…',
agentConfig: { id: 'claude', kind: 'terminal', /* …other fields pass through */ },
agent: 'claude', // host agent presetId, instance UUID, or 'superset' for built-in chat
rrule: 'FREQ=DAILY;BYHOUR=6;BYMINUTE=0',
timezone: 'America/Los_Angeles',
v2ProjectId: '<uuid>', // one of v2ProjectId or v2WorkspaceId required
Expand Down
12 changes: 5 additions & 7 deletions packages/cli/CLI_SPEC_TARGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,16 +602,15 @@ Output: `Automation` (with `recentRuns` omitted — use
| `--workspace <workspaceId>` | one of workspace/project | Reuse an existing workspace; project is derived server-side. |
| `--project <projectId>` | one of workspace/project | New-workspace-per-run mode. |
| `--host <hostId>` | no | Target host for runs. Default: owner's online host. |
| `--agent <presetId>` | no | Default: `claude`. |
| `--agent-config-file <path>` | no | Full ResolvedAgentConfig JSON; overrides `--agent`. |
| `--agent <agent>` | no | Host agent presetId, `HostAgentConfig` instance UUID, or `superset` for built-in chat. Default: `claude`. |

Exactly one of `--prompt` or `--prompt-file` must be provided. Exactly one
of `--workspace` or `--project` must be provided. Both constraints are
enforced at parse time and shown as `(required, one of: ...)` in help.

tRPC: `automation.create`.

Output: `Automation` (raw, including `id`, `nextRunAt`, `agentConfig`).
Output: `Automation` (raw, including `id`, `nextRunAt`, `agent`).

### `superset automations update <id>`

Expand All @@ -628,8 +627,7 @@ update semantics (see Backend Prerequisites).
| `--timezone <iana>` | |
| `--dtstart <iso8601>` | |
| `--host <hostId>` | Preserves the existing host when omitted. |
| `--agent <presetId>` | Preserves the existing agent config when omitted. |
| `--agent-config-file <path>` | Overrides `--agent` when both provided. |
| `--agent <agent>` | Host agent presetId, instance UUID, or `superset`. Preserves the existing value when omitted. |
| `--enabled` / `--no-enabled` | Calls `automation.setEnabled` first. |

tRPC:
Expand Down Expand Up @@ -812,8 +810,8 @@ These changes must land in the API/server before the v1 CLI ships:
- **`automation.create` workspace-only mode** — when `v2WorkspaceId` is
provided, derive `v2ProjectId` server-side instead of requiring both.
- **`automation.update` partial semantics** — treat `undefined` fields as
"no change" for `targetHostId` and `agentConfig`. The CLI will rely on
this to fix the silent-clobber bug (CLI-CURRENT-010, CLI-CURRENT-028).
"no change" for `targetHostId` and `agent`. The CLI will rely on this to
fix the silent-clobber bug (CLI-CURRENT-010, CLI-CURRENT-028).
- **`host.list`** on cloud — new tRPC procedure for the
`superset hosts list` discovery command. Returns hosts with
`id = machineId` (the consolidated identifier — see below).
Expand Down
Loading
Loading