diff --git a/components/backend/handlers/sessions.go b/components/backend/handlers/sessions.go index 2e3ee5611..659dbc2bf 100644 --- a/components/backend/handlers/sessions.go +++ b/components/backend/handlers/sessions.go @@ -1768,13 +1768,14 @@ func fetchGitHubDirectoryListing(ctx context.Context, owner, repo, ref, path, to // OOTBWorkflow represents an out-of-the-box workflow type OOTBWorkflow struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - GitURL string `json:"gitUrl"` - Branch string `json:"branch"` - Path string `json:"path,omitempty"` - Enabled bool `json:"enabled"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + GitURL string `json:"gitUrl"` + Branch string `json:"branch"` + Path string `json:"path,omitempty"` + Enabled bool `json:"enabled"` + StartupPrompt string `json:"startupPrompt,omitempty"` } // ListOOTBWorkflows returns the list of out-of-the-box workflows dynamically discovered from GitHub @@ -1883,8 +1884,9 @@ func ListOOTBWorkflows(c *gin.Context) { ambientData, err := fetchGitHubFileContent(c.Request.Context(), owner, repoName, ootbBranch, ambientPath, token) var ambientConfig struct { - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name"` + Description string `json:"description"` + StartupPrompt string `json:"startupPrompt"` } if err == nil { // Parse ambient.json if found @@ -1901,13 +1903,14 @@ func ListOOTBWorkflows(c *gin.Context) { } workflows = append(workflows, OOTBWorkflow{ - ID: entryName, - Name: workflowName, - Description: ambientConfig.Description, - GitURL: ootbRepo, - Branch: ootbBranch, - Path: fmt.Sprintf("%s/%s", ootbWorkflowsPath, entryName), - Enabled: true, + ID: entryName, + Name: workflowName, + Description: ambientConfig.Description, + GitURL: ootbRepo, + Branch: ootbBranch, + Path: fmt.Sprintf("%s/%s", ootbWorkflowsPath, entryName), + Enabled: true, + StartupPrompt: ambientConfig.StartupPrompt, }) } diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx index cdccd34dc..4f03498a5 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx @@ -24,6 +24,7 @@ type WelcomeExperienceProps = { hasRealMessages: boolean; onLoadWorkflow?: () => void; selectedWorkflow?: string; + workflowGreeting?: string | null; }; const WELCOME_MESSAGE = `Welcome to Ambient AI! Please select a workflow or type a message to get started.`; @@ -37,6 +38,7 @@ export function WelcomeExperience({ hasRealMessages, onLoadWorkflow, selectedWorkflow = "none", + workflowGreeting, }: WelcomeExperienceProps) { const [displayedText, setDisplayedText] = useState(""); const [isTypingComplete, setIsTypingComplete] = useState(false); @@ -181,6 +183,26 @@ export function WelcomeExperience({ + {/* Workflow greeting - rendered client-side after workflow activation */} + {workflowGreeting && ( +
+
+
+
+ AI +
+
+
+
+

+ {workflowGreeting} +

+
+
+
+
+ )} + {/* Workflow cards - show after typing completes (only for initial phases) */} {shouldShowWorkflowCards && isTypingComplete && enabledWorkflows.length > 0 && (
diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/hooks/use-workflow-management.ts b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/hooks/use-workflow-management.ts index 1bf7eb409..d96ca4fa7 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/hooks/use-workflow-management.ts +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/hooks/use-workflow-management.ts @@ -22,6 +22,7 @@ export function useWorkflowManagement({ const [pendingWorkflow, setPendingWorkflow] = useState(null); const [activeWorkflow, setActiveWorkflow] = useState(null); const [workflowActivating, setWorkflowActivating] = useState(false); + const [workflowGreeting, setWorkflowGreeting] = useState(null); // Use session queue for workflow persistence const sessionQueue = useSessionQueue(projectName, sessionName); @@ -85,12 +86,15 @@ export function useWorkflowManagement({ } setActiveWorkflow(workflow.id); + if (workflow.startupPrompt) { + setWorkflowGreeting(workflow.startupPrompt); + } setPendingWorkflow(null); sessionQueue.clearWorkflow(); - + // Wait for restart to complete (give runner time to clone and restart) await new Promise(resolve => setTimeout(resolve, 3000)); - + onWorkflowActivated?.(); setWorkflowActivating(false); @@ -158,6 +162,7 @@ export function useWorkflowManagement({ activeWorkflow, setActiveWorkflow, workflowActivating, + workflowGreeting, activateWorkflow, handleWorkflowChange, setCustomWorkflow, diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/lib/types.ts b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/lib/types.ts index 377e18773..7b57526af 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/lib/types.ts +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/lib/types.ts @@ -50,6 +50,7 @@ export type WorkflowConfig = { branch: string; path?: string; enabled: boolean; + startupPrompt?: string; }; export type WorkflowCommand = { diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index 3d6b0d57f..516dd0b3f 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -337,14 +337,17 @@ export default function ProjectSessionDetailPage({ const queuedWorkflow = workflowManagement.queuedWorkflow; if (phase === "Running" && queuedWorkflow && !queuedWorkflow.activatedAt) { // Session is now running, activate the queued workflow + // Look up the full workflow config (including startupPrompt) from the OOTB list + const fullWorkflow = ootbWorkflows.find(w => w.id === queuedWorkflow.id); workflowManagement.activateWorkflow({ id: queuedWorkflow.id, - name: "Queued workflow", - description: "", + name: fullWorkflow?.name || "Queued workflow", + description: fullWorkflow?.description || "", gitUrl: queuedWorkflow.gitUrl, branch: queuedWorkflow.branch, path: queuedWorkflow.path, enabled: true, + startupPrompt: fullWorkflow?.startupPrompt, }, phase); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -2589,6 +2592,7 @@ export default function ProjectSessionDetailPage({ hasRealMessages={hasRealMessages} onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)} selectedWorkflow={workflowManagement.selectedWorkflow} + workflowGreeting={workflowManagement.workflowGreeting} /> } /> @@ -2666,6 +2670,7 @@ export default function ProjectSessionDetailPage({ hasRealMessages={hasRealMessages} onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)} selectedWorkflow={workflowManagement.selectedWorkflow} + workflowGreeting={workflowManagement.workflowGreeting} /> } /> diff --git a/components/frontend/src/services/api/workflows.ts b/components/frontend/src/services/api/workflows.ts index 54d427b7d..4ee01c8da 100644 --- a/components/frontend/src/services/api/workflows.ts +++ b/components/frontend/src/services/api/workflows.ts @@ -8,6 +8,7 @@ export type OOTBWorkflow = { branch: string; path?: string; enabled: boolean; + startupPrompt?: string; }; export type ListOOTBWorkflowsResponse = { diff --git a/components/runners/claude-code-runner/ambient_runner/endpoints/workflow.py b/components/runners/claude-code-runner/ambient_runner/endpoints/workflow.py index 51d22c1c6..01e928483 100644 --- a/components/runners/claude-code-runner/ambient_runner/endpoints/workflow.py +++ b/components/runners/claude-code-runner/ambient_runner/endpoints/workflow.py @@ -5,14 +5,10 @@ import os import shutil import tempfile -import uuid from pathlib import Path -import aiohttp from fastapi import APIRouter, HTTPException, Request -from ambient_runner.platform.config import load_ambient_config - logger = logging.getLogger(__name__) router = APIRouter() @@ -23,7 +19,7 @@ @router.post("/workflow") async def change_workflow(request: Request): - """Change active workflow — triggers adapter reinit and greeting.""" + """Change active workflow — triggers adapter reinit.""" bridge = request.app.state.bridge context = bridge.context if not context: @@ -42,7 +38,7 @@ async def change_workflow(request: Request): current_path = os.getenv("ACTIVE_WORKFLOW_PATH", "").strip() if current_git_url == git_url and current_branch == branch and current_path == path: - logger.info("Workflow unchanged; skipping reinit and greeting") + logger.info("Workflow unchanged; skipping reinit") return {"message": "Workflow already active", "gitUrl": git_url, "branch": branch, "path": path} if git_url: @@ -57,7 +53,6 @@ async def change_workflow(request: Request): bridge.mark_dirty() logger.info("Workflow updated, adapter will reinitialize on next run") - asyncio.create_task(_trigger_workflow_greeting(git_url, branch, path, context)) return {"message": "Workflow updated", "gitUrl": git_url, "branch": branch, "path": path} @@ -133,61 +128,3 @@ async def clone_workflow_at_runtime(git_url: str, branch: str, subpath: str) -> finally: if temp_dir.exists(): shutil.rmtree(temp_dir, ignore_errors=True) - - -async def _trigger_workflow_greeting(git_url: str, branch: str, path: str, context): - """Send the workflow's startupPrompt (from ambient.json) after a workflow change. - - If the workflow has no startupPrompt, no greeting is sent. - """ - try: - workspace_path = os.getenv("WORKSPACE_PATH", "/workspace") - workflow_name = git_url.split("/")[-1].removesuffix(".git") - if path: - workflow_name = path.split("/")[-1] - - workflow_dir = str(Path(workspace_path) / "workflows" / workflow_name) - config = load_ambient_config(workflow_dir) if Path(workflow_dir).exists() else {} - startup_prompt = (config.get("startupPrompt") or "").strip() - - if not startup_prompt: - logger.info( - f"Workflow '{workflow_name}' has no startupPrompt in ambient.json, " - f"skipping greeting" - ) - return - - backend_url = os.getenv("BACKEND_API_URL", "").rstrip("/") - project_name = os.getenv("AGENTIC_SESSION_NAMESPACE", "").strip() - session_id = context.session_id if context else "unknown" - - if not backend_url or not project_name: - logger.error("Cannot trigger workflow greeting: BACKEND_API_URL or PROJECT_NAME not set") - return - - url = f"{backend_url}/projects/{project_name}/agentic-sessions/{session_id}/agui/run" - - payload = { - "threadId": session_id, - "runId": str(uuid.uuid4()), - "messages": [{ - "id": str(uuid.uuid4()), - "role": "user", - "content": startup_prompt, - "metadata": {"hidden": True, "autoSent": True, "source": "workflow_startup_prompt"}, - }], - } - - bot_token = os.getenv("BOT_TOKEN", "").strip() - headers = {"Content-Type": "application/json"} - if bot_token: - headers["Authorization"] = f"Bearer {bot_token}" - - async with aiohttp.ClientSession() as session: - async with session.post(url, json=payload, headers=headers) as resp: - if resp.status == 200: - logger.info(f"Workflow startupPrompt sent for '{workflow_name}'") - else: - logger.error(f"Workflow startupPrompt failed: {resp.status} - {await resp.text()}") - except Exception as e: - logger.error(f"Failed to send workflow startupPrompt: {e}")