From 9a5e401a3feabe4a240367167b590300128edc91 Mon Sep 17 00:00:00 2001 From: Mikolaj Graf Date: Fri, 24 Jul 2026 22:47:33 +0200 Subject: [PATCH 1/3] feat subagent fanout to subworkspaces --- .../main/lib/agent-setup/notify-hook.test.ts | 111 +- .../src/main/lib/agent-setup/notify-hook.ts | 8 +- .../templates/notify-hook.template.sh | 102 +- .../src/renderer/commandPalette/core/types.ts | 2 +- .../useHostWorkspaces/index.ts | 1 + .../useHostWorkspaces.utils.ts | 19 +- .../DashboardSidebarDeleteDialog.tsx | 2 + .../DestroyConfirmPane/DestroyConfirmPane.tsx | 41 +- .../useDestroyDialogState.ts | 11 +- .../DashboardSidebarExpandedWorkspaceRow.tsx | 25 +- .../buildDashboardSidebarProjects.test.ts | 78 ++ .../buildDashboardSidebarProjects.ts | 65 +- .../useDashboardSidebarData.ts | 66 +- .../components/DashboardSidebar/types.ts | 6 +- .../components/AddTabMenu/AddTabMenu.tsx | 14 + .../useAutoAdoptBackgroundSessions.ts | 59 +- .../useConsumeAutomationRunLink.ts | 29 +- .../v2-workspace/$workspaceId/page.tsx | 13 + .../utils/focusChatPane/focusChatPane.ts | 29 + .../$workspaceId/utils/focusChatPane/index.ts | 1 + .../useRemoteHostStatus.ts | 4 +- .../WorkspaceProvider/WorkspaceProvider.tsx | 6 +- .../useAccessibleV2Workspaces.ts | 2 +- .../selectWorktreesToPlace.ts | 2 +- .../PromptGroup/PromptGroup.tsx | 30 + .../useAgentDelegationPreference/index.ts | 1 + .../useAgentDelegationPreference.test.ts | 32 + .../useAgentDelegationPreference.ts | 40 + .../useSubmitWorkspace/useSubmitWorkspace.ts | 5 + .../sidebarMutations.ts | 11 +- .../useVisibleSidebarWorkspaceIds.ts | 13 + .../sidebarVisibility.test.ts | 56 + .../sidebarVisibility.ts | 49 + .../workspace-creates/useWorkspaceCreates.ts | 2 + .../getV2WorkspaceDisplayName.ts | 2 +- .../workspaces/create-subworkspace/command.ts | 62 ++ packages/cli/src/lib/host-target/index.ts | 5 + .../src/lib/host-target/resolveHostTarget.ts | 37 +- .../resolveLocalWorkspaceTarget.ts | 51 + packages/cli/src/lib/host/manifest.test.ts | 43 + packages/cli/src/lib/host/manifest.ts | 31 +- .../drizzle/0012_clean_edwin_jarvis.sql | 2 + .../0013_persist_agent_delegation_mode.sql | 1 + .../drizzle/meta/0012_snapshot.json | 954 +++++++++++++++++ .../drizzle/meta/0013_snapshot.json | 962 ++++++++++++++++++ .../host-service/drizzle/meta/_journal.json | 14 + packages/host-service/src/db/schema.ts | 16 +- packages/host-service/src/events/types.ts | 5 +- .../host-service/src/terminal/env.test.ts | 3 + packages/host-service/src/terminal/env.ts | 14 +- .../host-service/src/terminal/terminal.ts | 1 + .../src/trpc/router/agents/agents.ts | 23 +- .../notifications/notifications.test.ts | 36 +- .../router/notifications/notifications.ts | 19 +- .../workspace-cleanup/is-main-workspace.ts | 7 + .../workspace-cleanup/workspace-cleanup.ts | 106 +- .../shared/adopt-existing-worktree.ts | 9 +- .../src/trpc/router/workspace/workspace.ts | 20 +- .../src/trpc/router/workspaces/workspaces.ts | 102 +- .../workspaces/local-workspace-store.test.ts | 134 +++ .../src/workspaces/local-workspace-store.ts | 38 +- packages/host-service/test/helpers/seed.ts | 4 +- .../subworkspace.integration.test.ts | 67 ++ .../test/workspace-cleanup.test.ts | 2 +- packages/sdk/src/resources/workspaces.ts | 8 +- packages/shared/package.json | 4 + packages/shared/src/agent-delegation.test.ts | 22 + packages/shared/src/agent-delegation.ts | 28 + 68 files changed, 3594 insertions(+), 173 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/focusChatPane/focusChatPane.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/focusChatPane/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useAgentDelegationPreference/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useAgentDelegationPreference/useAgentDelegationPreference.test.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useAgentDelegationPreference/useAgentDelegationPreference.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/sidebarVisibility.test.ts create mode 100644 packages/cli/src/commands/workspaces/create-subworkspace/command.ts create mode 100644 packages/cli/src/lib/host-target/resolveLocalWorkspaceTarget.ts create mode 100644 packages/cli/src/lib/host/manifest.test.ts create mode 100644 packages/host-service/drizzle/0012_clean_edwin_jarvis.sql create mode 100644 packages/host-service/drizzle/0013_persist_agent_delegation_mode.sql create mode 100644 packages/host-service/drizzle/meta/0012_snapshot.json create mode 100644 packages/host-service/drizzle/meta/0013_snapshot.json create mode 100644 packages/host-service/src/workspaces/local-workspace-store.test.ts create mode 100644 packages/host-service/test/integration/subworkspace.integration.test.ts create mode 100644 packages/shared/src/agent-delegation.test.ts create mode 100644 packages/shared/src/agent-delegation.ts diff --git a/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts b/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts index 22ea30e5dc5..fd25d0a6045 100644 --- a/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from "bun:test"; -import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; -import { NOTIFY_SCRIPT_MARKER } from "./notify-hook"; +import { getNotifyScriptContent, NOTIFY_SCRIPT_MARKER } from "./notify-hook"; describe("getNotifyScriptContent", () => { it("bumps the notify hook marker when hook semantics change", () => { - expect(NOTIFY_SCRIPT_MARKER).toBe("# Superset agent notification hook v3"); + expect(NOTIFY_SCRIPT_MARKER).toBe("# Superset agent notification hook v4"); }); it("emits the v2 host-service payload with full agent identity", () => { @@ -36,6 +38,107 @@ describe("getNotifyScriptContent", () => { ); }); + it("emits hidden UserPromptSubmit context when workspace fan-out is enabled", () => { + const result = spawnSync( + "bash", + ["-s", "--", '{"hook_event_name":"UserPromptSubmit"}'], + { + input: getNotifyScriptContent(), + encoding: "utf8", + env: { + PATH: process.env.PATH, + SUPERSET_AGENT_DELEGATION_MODE: "workspaces", + SUPERSET_AGENT_ID: "codex", + SUPERSET_WORKSPACE_ID: "workspace-123", + }, + }, + ); + + expect(result.status).toBe(0); + const output = JSON.parse(result.stdout); + expect(output).toMatchObject({ + hookSpecificOutput: { + hookEventName: "UserPromptSubmit", + }, + }); + expect(output.hookSpecificOutput.additionalContext).toContain( + "superset workspaces create-subworkspace", + ); + expect(output.hookSpecificOutput.additionalContext).toContain( + '--parent "workspace-123"', + ); + expect(output.hookSpecificOutput.additionalContext).toContain( + '--agent "codex"', + ); + }); + + it("does not emit delegation context in native mode", () => { + const result = spawnSync( + "bash", + ["-s", "--", '{"hook_event_name":"UserPromptSubmit"}'], + { + input: getNotifyScriptContent(), + encoding: "utf8", + env: { + PATH: process.env.PATH, + SUPERSET_AGENT_DELEGATION_MODE: "native", + SUPERSET_AGENT_ID: "codex", + SUPERSET_WORKSPACE_ID: "workspace-123", + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + }); + + it("uses the host's current mode for an already-open terminal", () => { + const fakeBinDir = mkdtempSync( + path.join(tmpdir(), "superset-notify-hook-test-"), + ); + try { + writeFileSync( + path.join(fakeBinDir, "curl"), + `#!/bin/bash +while [ "$#" -gt 0 ]; do + case "$1" in + -o) OUTPUT_FILE="$2"; shift 2 ;; + *) shift ;; + esac +done +printf '%s' '{"result":{"data":{"json":{"agentDelegationMode":"workspaces"}}}}' > "$OUTPUT_FILE" +printf '200' +`, + { mode: 0o755 }, + ); + + const result = spawnSync( + "bash", + ["-s", "--", '{"hook_event_name":"UserPromptSubmit"}'], + { + input: getNotifyScriptContent(), + encoding: "utf8", + env: { + PATH: `${fakeBinDir}:${process.env.PATH}`, + SUPERSET_AGENT_DELEGATION_MODE: "native", + SUPERSET_AGENT_ID: "codex", + SUPERSET_HOST_AGENT_HOOK_URL: "http://127.0.0.1/hook", + SUPERSET_TERMINAL_ID: "terminal-123", + SUPERSET_WORKSPACE_ID: "workspace-123", + }, + }, + ); + + expect(result.status).toBe(0); + const output = JSON.parse(result.stdout); + expect(output.hookSpecificOutput.additionalContext).toContain( + '--parent "workspace-123"', + ); + } finally { + rmSync(fakeBinDir, { recursive: true, force: true }); + } + }); + it("falls back to the v1 Electron hook when v2 is unavailable", () => { const script = readFileSync( path.join(import.meta.dir, "templates", "notify-hook.template.sh"), @@ -46,7 +149,7 @@ describe("getNotifyScriptContent", () => { 'if [ -n "$SUPERSET_HOST_AGENT_HOOK_URL" ] && [ -n "$SUPERSET_TERMINAL_ID" ]; then', ); expect(script).toContain( - '[ -z "$SUPERSET_TAB_ID" ] && [ -z "$SESSION_ID" ] && [ -z "$SUPERSET_TERMINAL_ID" ] && exit 0', + 'if [ -n "$SUPERSET_TAB_ID" ] || [ -n "$SESSION_ID" ] || [ -n "$SUPERSET_TERMINAL_ID" ]; then', ); expect(script).toContain("/hook/complete"); expect(script).toContain("terminalId=$SUPERSET_TERMINAL_ID"); diff --git a/apps/desktop/src/main/lib/agent-setup/notify-hook.ts b/apps/desktop/src/main/lib/agent-setup/notify-hook.ts index 4dda200742e..0293d4da196 100644 --- a/apps/desktop/src/main/lib/agent-setup/notify-hook.ts +++ b/apps/desktop/src/main/lib/agent-setup/notify-hook.ts @@ -1,10 +1,11 @@ import fs from "node:fs"; import path from "node:path"; +import { buildWorkspaceDelegationInstructions } from "@superset/shared/agent-delegation"; import { env } from "shared/env.shared"; import { HOOKS_DIR } from "./paths"; export const NOTIFY_SCRIPT_NAME = "notify.sh"; -export const NOTIFY_SCRIPT_MARKER = "# Superset agent notification hook v3"; +export const NOTIFY_SCRIPT_MARKER = "# Superset agent notification hook v4"; const NOTIFY_SCRIPT_TEMPLATE_PATH = path.join( __dirname, @@ -39,8 +40,13 @@ export function getNotifyScriptPath(): string { export function getNotifyScriptContent(): string { const template = fs.readFileSync(NOTIFY_SCRIPT_TEMPLATE_PATH, "utf-8"); + const delegationContext = buildWorkspaceDelegationInstructions({ + workspaceId: "__SUPERSET_WORKSPACE_ID__", + agent: "__SUPERSET_AGENT_ID__", + }); return template .replaceAll("{{MARKER}}", NOTIFY_SCRIPT_MARKER) + .replaceAll("{{DELEGATION_CONTEXT}}", delegationContext) .replaceAll("{{DEFAULT_PORT}}", String(env.DESKTOP_NOTIFICATIONS_PORT)); } diff --git a/apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh b/apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh index 79c059e2f60..76d7c448f0e 100644 --- a/apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh +++ b/apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh @@ -31,6 +31,8 @@ if [ -z "$EVENT_TYPE" ]; then esac fi +RAW_EVENT_TYPE="$EVENT_TYPE" + # UserPromptSubmit normalizes here; other aliases are mapped server-side # by mapEventType so the wire stays a single source of truth. [ "$EVENT_TYPE" = "UserPromptSubmit" ] && EVENT_TYPE="Start" @@ -70,62 +72,90 @@ case "$V1_EVENT_TYPE" in esac json_escape() { - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' + local value="$1" + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//$'\n'/\\n} + value=${value//$'\r'/\\r} + value=${value//$'\t'/\\t} + printf '%s' "$value" } +DELEGATION_MODE="${SUPERSET_AGENT_DELEGATION_MODE:-native}" + if [ -n "$SUPERSET_HOST_AGENT_HOOK_URL" ] && [ -n "$SUPERSET_TERMINAL_ID" ]; then PAYLOAD="{\"json\":{\"terminalId\":\"$(json_escape "$SUPERSET_TERMINAL_ID")\",\"eventType\":\"$(json_escape "$EVENT_TYPE")\",\"agent\":{\"agentId\":\"$(json_escape "$SUPERSET_AGENT_ID")\",\"sessionId\":\"$(json_escape "$SESSION_ID")\"}}}" + RESPONSE_FILE=$(mktemp "${TMPDIR:-/tmp}/superset-agent-hook.XXXXXX") STATUS_CODE=$(curl -sX POST "$SUPERSET_HOST_AGENT_HOOK_URL" \ --connect-timeout 2 --max-time 5 \ -H "Content-Type: application/json" \ -d "$PAYLOAD" \ - -o /dev/null -w "%{http_code}" 2>/dev/null) + -o "$RESPONSE_FILE" -w "%{http_code}" 2>/dev/null) + RESPONSE=$(cat "$RESPONSE_FILE" 2>/dev/null) + rm -f "$RESPONSE_FILE" + HOST_DELEGATION_MODE=$(printf '%s' "$RESPONSE" | grep -oE '"agentDelegationMode"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"') + if [ -n "$HOST_DELEGATION_MODE" ]; then + DELEGATION_MODE="$HOST_DELEGATION_MODE" + fi if [ "$DEBUG_HOOKS_ENABLED" = "1" ]; then - echo "[notify-hook] host-service dispatched status=$STATUS_CODE" >&2 + echo "[notify-hook] host-service dispatched status=$STATUS_CODE delegationMode=$DELEGATION_MODE" >&2 fi debug_log "host-service status=$STATUS_CODE url=$SUPERSET_HOST_AGENT_HOOK_URL" case "$STATUS_CODE" in - 2*) exit 0 ;; + 2*) HOST_DISPATCHED="1" ;; esac fi # v1 fallback: Electron localhost hook server. Kept while v1 terminals exist. -[ -z "$SUPERSET_TAB_ID" ] && [ -z "$SESSION_ID" ] && [ -z "$SUPERSET_TERMINAL_ID" ] && exit 0 +if [ "${HOST_DISPATCHED:-0}" != "1" ]; then + if [ -n "$SUPERSET_TAB_ID" ] || [ -n "$SESSION_ID" ] || [ -n "$SUPERSET_TERMINAL_ID" ]; then + + if [ "$DEBUG_HOOKS_ENABLED" = "1" ]; then + STATUS_CODE=$(curl -sG "http://127.0.0.1:${SUPERSET_PORT:-{{DEFAULT_PORT}}}/hook/complete" \ + --connect-timeout 1 --max-time 2 \ + --data-urlencode "paneId=$SUPERSET_PANE_ID" \ + --data-urlencode "tabId=$SUPERSET_TAB_ID" \ + --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \ + --data-urlencode "terminalId=$SUPERSET_TERMINAL_ID" \ + --data-urlencode "sessionId=$SESSION_ID" \ + --data-urlencode "hookSessionId=$HOOK_SESSION_ID" \ + --data-urlencode "resourceId=$RESOURCE_ID" \ + --data-urlencode "eventType=$V1_EVENT_TYPE" \ + --data-urlencode "env=$SUPERSET_ENV" \ + --data-urlencode "version=$SUPERSET_HOOK_VERSION" \ + -o /dev/null -w "%{http_code}" 2>/dev/null) + echo "[notify-hook] v1 dispatched status=$STATUS_CODE" >&2 + debug_log "v1 status=$STATUS_CODE port=${SUPERSET_PORT:-{{DEFAULT_PORT}}}" + else + debug_log "v1 dispatch port=${SUPERSET_PORT:-{{DEFAULT_PORT}}}" + curl -sG "http://127.0.0.1:${SUPERSET_PORT:-{{DEFAULT_PORT}}}/hook/complete" \ + --connect-timeout 1 --max-time 2 \ + --data-urlencode "paneId=$SUPERSET_PANE_ID" \ + --data-urlencode "tabId=$SUPERSET_TAB_ID" \ + --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \ + --data-urlencode "terminalId=$SUPERSET_TERMINAL_ID" \ + --data-urlencode "sessionId=$SESSION_ID" \ + --data-urlencode "hookSessionId=$HOOK_SESSION_ID" \ + --data-urlencode "resourceId=$RESOURCE_ID" \ + --data-urlencode "eventType=$V1_EVENT_TYPE" \ + --data-urlencode "env=$SUPERSET_ENV" \ + --data-urlencode "version=$SUPERSET_HOOK_VERSION" \ + > /dev/null 2>&1 + fi + fi +fi -if [ "$DEBUG_HOOKS_ENABLED" = "1" ]; then - STATUS_CODE=$(curl -sG "http://127.0.0.1:${SUPERSET_PORT:-{{DEFAULT_PORT}}}/hook/complete" \ - --connect-timeout 1 --max-time 2 \ - --data-urlencode "paneId=$SUPERSET_PANE_ID" \ - --data-urlencode "tabId=$SUPERSET_TAB_ID" \ - --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \ - --data-urlencode "terminalId=$SUPERSET_TERMINAL_ID" \ - --data-urlencode "sessionId=$SESSION_ID" \ - --data-urlencode "hookSessionId=$HOOK_SESSION_ID" \ - --data-urlencode "resourceId=$RESOURCE_ID" \ - --data-urlencode "eventType=$V1_EVENT_TYPE" \ - --data-urlencode "env=$SUPERSET_ENV" \ - --data-urlencode "version=$SUPERSET_HOOK_VERSION" \ - -o /dev/null -w "%{http_code}" 2>/dev/null) - echo "[notify-hook] v1 dispatched status=$STATUS_CODE" >&2 - debug_log "v1 status=$STATUS_CODE port=${SUPERSET_PORT:-{{DEFAULT_PORT}}}" -else - debug_log "v1 dispatch port=${SUPERSET_PORT:-{{DEFAULT_PORT}}}" - curl -sG "http://127.0.0.1:${SUPERSET_PORT:-{{DEFAULT_PORT}}}/hook/complete" \ - --connect-timeout 1 --max-time 2 \ - --data-urlencode "paneId=$SUPERSET_PANE_ID" \ - --data-urlencode "tabId=$SUPERSET_TAB_ID" \ - --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \ - --data-urlencode "terminalId=$SUPERSET_TERMINAL_ID" \ - --data-urlencode "sessionId=$SESSION_ID" \ - --data-urlencode "hookSessionId=$HOOK_SESSION_ID" \ - --data-urlencode "resourceId=$RESOURCE_ID" \ - --data-urlencode "eventType=$V1_EVENT_TYPE" \ - --data-urlencode "env=$SUPERSET_ENV" \ - --data-urlencode "version=$SUPERSET_HOOK_VERSION" \ - > /dev/null 2>&1 +if [ "$RAW_EVENT_TYPE" = "UserPromptSubmit" ] && [ "$DELEGATION_MODE" = "workspaces" ]; then + DELEGATION_CONTEXT=$(cat <<'SUPERSET_DELEGATION_CONTEXT' +{{DELEGATION_CONTEXT}} +SUPERSET_DELEGATION_CONTEXT +) + DELEGATION_CONTEXT=${DELEGATION_CONTEXT//__SUPERSET_WORKSPACE_ID__/$SUPERSET_WORKSPACE_ID} + DELEGATION_CONTEXT=${DELEGATION_CONTEXT//__SUPERSET_AGENT_ID__/$SUPERSET_AGENT_ID} + printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$(json_escape "$DELEGATION_CONTEXT")" fi exit 0 diff --git a/apps/desktop/src/renderer/commandPalette/core/types.ts b/apps/desktop/src/renderer/commandPalette/core/types.ts index 607643957e6..99ae9420a75 100644 --- a/apps/desktop/src/renderer/commandPalette/core/types.ts +++ b/apps/desktop/src/renderer/commandPalette/core/types.ts @@ -14,7 +14,7 @@ export interface CommandContext { id: string; name: string; projectId?: string; - workspaceType?: "main" | "worktree"; + workspaceType?: "main" | "worktree" | "subworkspace"; hostId?: string; preferredOpenInApp?: ExternalApp; } | null; diff --git a/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/index.ts b/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/index.ts index 6a239f57150..d10cf19cb22 100644 --- a/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/index.ts +++ b/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/index.ts @@ -3,4 +3,5 @@ export { useHostWorkspacesSource } from "./useHostWorkspaces"; export type { HostWorkspaceItem, HostWorkspaceRow, + HostWorkspaceType, } from "./useHostWorkspaces.utils"; diff --git a/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.ts b/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.ts index 80a7976fd2f..5011e2a93ac 100644 --- a/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.ts +++ b/apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.ts @@ -1,4 +1,5 @@ import type { SelectV2Workspace } from "@superset/db/schema"; +import type { AgentDelegationMode } from "@superset/shared/agent-delegation"; import { buildHostRoutingKey } from "@superset/shared/host-routing"; import type { WorkspaceSnapshotPayload } from "@superset/workspace-client"; import { del as idbDel, get as idbGet, set as idbSet } from "idb-keyval"; @@ -7,13 +8,21 @@ import { del as idbDel, get as idbGet, set as idbSet } from "idb-keyval"; * A workspace row as served by a host (`workspace.list`) — the cloud row * shape plus the host-only extras. */ -export interface HostWorkspaceRow extends SelectV2Workspace { +export type HostWorkspaceType = "main" | "worktree" | "subworkspace"; + +export interface HostWorkspaceRow extends Omit { + type: HostWorkspaceType; + parentWorkspaceId: string | null; + agentDelegationMode: AgentDelegationMode; worktreePath: string; worktreeExists: boolean; } /** Merged item returned by useHostWorkspaces. */ -export interface HostWorkspaceItem extends SelectV2Workspace { +export interface HostWorkspaceItem extends Omit { + type: HostWorkspaceType; + parentWorkspaceId: string | null; + agentDelegationMode: AgentDelegationMode; worktreePath?: string; worktreeExists?: boolean; /** False when the row came from a snapshot/cloud and the host didn't answer. */ @@ -170,6 +179,8 @@ export function applyWorkspaceChangedEvent( name: snapshot.name, branch: snapshot.branch, type: snapshot.type, + parentWorkspaceId: snapshot.parentWorkspaceId, + agentDelegationMode: snapshot.agentDelegationMode, createdByUserId: snapshot.createdByUserId, taskId: snapshot.taskId, createdAt: new Date(snapshot.createdAt), @@ -215,6 +226,8 @@ export function mergeHostWorkspaces({ seenIds.add(row.id); items.push({ ...row, + parentWorkspaceId: row.parentWorkspaceId ?? null, + agentDelegationMode: row.agentDelegationMode ?? "native", hostReachable: result.reachable, source: "host", }); @@ -226,6 +239,8 @@ export function mergeHostWorkspaces({ seenIds.add(row.id); items.push({ ...row, + parentWorkspaceId: null, + agentDelegationMode: "native", hostReachable: false, source: "cloud", }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 9aeea067dc1..239d129c9c9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -26,6 +26,7 @@ export function DashboardSidebarDeleteDialog({ }: DashboardSidebarDeleteDialogProps) { const { deleteBranch, + isSubworkspace, setDeleteBranch, hasChanges, hasUnpushedCommits, @@ -61,6 +62,7 @@ export function DashboardSidebarDeleteDialog({ open={open} onOpenChange={handleOpenChange} workspaceName={workspaceName} + isSubworkspace={isSubworkspace} deleteBranch={deleteBranch} onDeleteBranchChange={setDeleteBranch} hasChanges={hasChanges} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx index ba21c967244..bcb07c65451 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx @@ -16,6 +16,7 @@ interface DestroyConfirmPaneProps { open: boolean; onOpenChange: (open: boolean) => void; workspaceName: string; + isSubworkspace: boolean; deleteBranch: boolean; onDeleteBranchChange: (next: boolean) => void; hasChanges: boolean; @@ -30,6 +31,7 @@ export function DestroyConfirmPane({ open, onOpenChange, workspaceName, + isSubworkspace, deleteBranch, onDeleteBranchChange, hasChanges, @@ -63,8 +65,9 @@ export function DestroyConfirmPane({ Delete workspace "{workspaceName}"? - This removes the worktree from disk. The cloud workspace record will - also be removed. + {isSubworkspace + ? "This removes the subworkspace and its agent sessions. The shared working tree and branch are unchanged." + : "This removes the worktree from disk. The cloud workspace record will also be removed."}
@@ -92,23 +95,25 @@ export function DestroyConfirmPane({
)} -
-
- - onDeleteBranchChange(checked === true) - } - /> - + {!isSubworkspace && ( +
+
+ + onDeleteBranchChange(checked === true) + } + /> + +
-
+ )}