Skip to content
Open
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
35 changes: 19 additions & 16 deletions components/backend/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.`;
Expand All @@ -37,6 +38,7 @@ export function WelcomeExperience({
hasRealMessages,
onLoadWorkflow,
selectedWorkflow = "none",
workflowGreeting,
}: WelcomeExperienceProps) {
const [displayedText, setDisplayedText] = useState("");
const [isTypingComplete, setIsTypingComplete] = useState(false);
Expand Down Expand Up @@ -181,6 +183,26 @@ export function WelcomeExperience({
</div>
</div>

{/* Workflow greeting - rendered client-side after workflow activation */}
{workflowGreeting && (
<div className="mb-4">
<div className="flex space-x-3 items-start">
<div className="flex-shrink-0">
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-blue-600">
<span className="text-white text-xs font-semibold">AI</span>
</div>
</div>
<div className="flex-1 min-w-0">
<div className="rounded-lg bg-card">
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap mb-[0.2rem]">
{workflowGreeting}
</p>
</div>
</div>
</div>
</div>
)}

{/* Workflow cards - show after typing completes (only for initial phases) */}
{shouldShowWorkflowCards && isTypingComplete && enabledWorkflows.length > 0 && (
<div className="pl-11 pr-4 space-y-2 animate-fade-in-up">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function useWorkflowManagement({
const [pendingWorkflow, setPendingWorkflow] = useState<WorkflowConfig | null>(null);
const [activeWorkflow, setActiveWorkflow] = useState<string | null>(null);
const [workflowActivating, setWorkflowActivating] = useState(false);
const [workflowGreeting, setWorkflowGreeting] = useState<string | null>(null);

// Use session queue for workflow persistence
const sessionQueue = useSessionQueue(projectName, sessionName);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -158,6 +162,7 @@ export function useWorkflowManagement({
activeWorkflow,
setActiveWorkflow,
workflowActivating,
workflowGreeting,
activateWorkflow,
handleWorkflowChange,
setCustomWorkflow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type WorkflowConfig = {
branch: string;
path?: string;
enabled: boolean;
startupPrompt?: string;
};

export type WorkflowCommand = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2589,6 +2592,7 @@ export default function ProjectSessionDetailPage({
hasRealMessages={hasRealMessages}
onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)}
selectedWorkflow={workflowManagement.selectedWorkflow}
workflowGreeting={workflowManagement.workflowGreeting}
/>
}
/>
Expand Down Expand Up @@ -2666,6 +2670,7 @@ export default function ProjectSessionDetailPage({
hasRealMessages={hasRealMessages}
onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)}
selectedWorkflow={workflowManagement.selectedWorkflow}
workflowGreeting={workflowManagement.workflowGreeting}
/>
}
/>
Expand Down
1 change: 1 addition & 0 deletions components/frontend/src/services/api/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type OOTBWorkflow = {
branch: string;
path?: string;
enabled: boolean;
startupPrompt?: string;
};

export type ListOOTBWorkflowsResponse = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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}

Expand Down Expand Up @@ -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}")