Skip to content

Commit 143e209

Browse files
committed
feat: add workflow results endpoints and UI integration
- Implemented new backend endpoints for retrieving workflow results: - `GET /content/workflow-results` for fetching results based on session. - `GET /api/projects/:projectName/agentic-sessions/:sessionName/workflow/results` for project-specific results. - Updated frontend to display workflow results in a tabbed interface, allowing users to view output files and their statuses. - Enhanced the `useWorkflowResults` hook for fetching results with polling every 5 seconds. These changes improve the user experience by providing access to workflow output files directly within the session interface.
1 parent 33d1575 commit 143e209

File tree

11 files changed

+565
-35
lines changed

11 files changed

+565
-35
lines changed

components/backend/handlers/content.go

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/base64"
66
"encoding/json"
7+
"fmt"
78
"log"
89
"net/http"
910
"os"
@@ -601,10 +602,11 @@ func parseFrontmatter(filePath string) map[string]string {
601602

602603
// AmbientConfig represents the ambient.json configuration
603604
type AmbientConfig struct {
604-
Name string `json:"name"`
605-
Description string `json:"description"`
606-
SystemPrompt string `json:"systemPrompt"`
607-
ArtifactsDir string `json:"artifactsDir"`
605+
Name string `json:"name"`
606+
Description string `json:"description"`
607+
SystemPrompt string `json:"systemPrompt"`
608+
ArtifactsDir string `json:"artifactsDir"`
609+
Results map[string]string `json:"results,omitempty"` // displayName -> glob pattern
608610
}
609611

610612
// parseAmbientConfig reads and parses ambient.json from workflow directory
@@ -640,6 +642,83 @@ func parseAmbientConfig(workflowDir string) *AmbientConfig {
640642
return &config
641643
}
642644

645+
// ResultFile represents a workflow result file
646+
type ResultFile struct {
647+
DisplayName string `json:"displayName"`
648+
Path string `json:"path"` // Relative path from workspace
649+
Exists bool `json:"exists"`
650+
Content string `json:"content,omitempty"`
651+
Error string `json:"error,omitempty"`
652+
}
653+
654+
// ContentWorkflowResults handles GET /content/workflow-results?session=
655+
func ContentWorkflowResults(c *gin.Context) {
656+
sessionName := c.Query("session")
657+
if sessionName == "" {
658+
c.JSON(http.StatusBadRequest, gin.H{"error": "missing session parameter"})
659+
return
660+
}
661+
662+
workflowDir := findActiveWorkflowDir(sessionName)
663+
if workflowDir == "" {
664+
c.JSON(http.StatusOK, gin.H{"results": []ResultFile{}})
665+
return
666+
}
667+
668+
ambientConfig := parseAmbientConfig(workflowDir)
669+
if len(ambientConfig.Results) == 0 {
670+
c.JSON(http.StatusOK, gin.H{"results": []ResultFile{}})
671+
return
672+
}
673+
674+
workspaceBase := filepath.Join(StateBaseDir, "sessions", sessionName, "workspace")
675+
results := []ResultFile{}
676+
677+
for displayName, pattern := range ambientConfig.Results {
678+
absPattern := filepath.Join(workspaceBase, pattern)
679+
matches, err := filepath.Glob(absPattern)
680+
681+
if err != nil {
682+
results = append(results, ResultFile{
683+
DisplayName: displayName,
684+
Path: pattern,
685+
Exists: false,
686+
Error: fmt.Sprintf("Invalid pattern: %v", err),
687+
})
688+
continue
689+
}
690+
691+
if len(matches) == 0 {
692+
results = append(results, ResultFile{
693+
DisplayName: displayName,
694+
Path: pattern,
695+
Exists: false,
696+
})
697+
} else {
698+
for _, matchedPath := range matches {
699+
relPath, _ := filepath.Rel(workspaceBase, matchedPath)
700+
content, readErr := os.ReadFile(matchedPath)
701+
702+
result := ResultFile{
703+
DisplayName: displayName,
704+
Path: relPath,
705+
Exists: true,
706+
}
707+
708+
if readErr != nil {
709+
result.Error = fmt.Sprintf("Failed to read: %v", readErr)
710+
} else {
711+
result.Content = string(content)
712+
}
713+
714+
results = append(results, result)
715+
}
716+
}
717+
}
718+
719+
c.JSON(http.StatusOK, gin.H{"results": results})
720+
}
721+
643722
// findActiveWorkflowDir finds the active workflow directory for a session
644723
func findActiveWorkflowDir(sessionName string) string {
645724
// Workflows are stored at {StateBaseDir}/sessions/{session-name}/workspace/workflows/{workflow-name}

components/backend/handlers/sessions.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,64 @@ func GetWorkflowMetadata(c *gin.Context) {
13291329
c.Data(resp.StatusCode, "application/json", b)
13301330
}
13311331

1332+
// GetWorkflowResults retrieves workflow result files from the active workflow
1333+
// GET /api/projects/:projectName/agentic-sessions/:sessionName/workflow/results
1334+
func GetWorkflowResults(c *gin.Context) {
1335+
project := c.GetString("project")
1336+
if project == "" {
1337+
project = c.Param("projectName")
1338+
}
1339+
sessionName := c.Param("sessionName")
1340+
1341+
if project == "" {
1342+
log.Printf("GetWorkflowResults: project is empty, session=%s", sessionName)
1343+
c.JSON(http.StatusBadRequest, gin.H{"error": "Project namespace required"})
1344+
return
1345+
}
1346+
1347+
// Get authorization token
1348+
token := c.GetHeader("Authorization")
1349+
if strings.TrimSpace(token) == "" {
1350+
token = c.GetHeader("X-Forwarded-Access-Token")
1351+
}
1352+
1353+
// Try temp service first (for completed sessions), then regular service
1354+
serviceName := fmt.Sprintf("temp-content-%s", sessionName)
1355+
reqK8s, _ := GetK8sClientsForRequest(c)
1356+
if reqK8s != nil {
1357+
if _, err := reqK8s.CoreV1().Services(project).Get(c.Request.Context(), serviceName, v1.GetOptions{}); err != nil {
1358+
// Temp service doesn't exist, use regular service
1359+
serviceName = fmt.Sprintf("ambient-content-%s", sessionName)
1360+
}
1361+
} else {
1362+
serviceName = fmt.Sprintf("ambient-content-%s", sessionName)
1363+
}
1364+
1365+
// Build URL to content service
1366+
endpoint := fmt.Sprintf("http://%s.%s.svc:8080", serviceName, project)
1367+
u := fmt.Sprintf("%s/content/workflow-results?session=%s", endpoint, sessionName)
1368+
1369+
log.Printf("GetWorkflowResults: project=%s session=%s endpoint=%s", project, sessionName, endpoint)
1370+
1371+
// Create and send request to content pod
1372+
req, _ := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, u, nil)
1373+
if strings.TrimSpace(token) != "" {
1374+
req.Header.Set("Authorization", token)
1375+
}
1376+
client := &http.Client{Timeout: 4 * time.Second}
1377+
resp, err := client.Do(req)
1378+
if err != nil {
1379+
log.Printf("GetWorkflowResults: content service request failed: %v", err)
1380+
// Return empty results on error
1381+
c.JSON(http.StatusOK, gin.H{"results": []interface{}{}})
1382+
return
1383+
}
1384+
defer resp.Body.Close()
1385+
1386+
b, _ := io.ReadAll(resp.Body)
1387+
c.Data(resp.StatusCode, "application/json", b)
1388+
}
1389+
13321390
// fetchGitHubFileContent fetches a file from GitHub via API
13331391
// token is optional - works for public repos without authentication (but has rate limits)
13341392
func fetchGitHubFileContent(ctx context.Context, owner, repo, ref, path, token string) ([]byte, error) {

components/backend/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func registerContentRoutes(r *gin.Engine) {
1818
r.POST("/content/git-configure-remote", handlers.ContentGitConfigureRemote)
1919
r.POST("/content/git-sync", handlers.ContentGitSync)
2020
r.GET("/content/workflow-metadata", handlers.ContentWorkflowMetadata)
21+
r.GET("/content/workflow-results", handlers.ContentWorkflowResults)
2122
r.GET("/content/git-merge-status", handlers.ContentGitMergeStatus)
2223
r.POST("/content/git-pull", handlers.ContentGitPull)
2324
r.POST("/content/git-push", handlers.ContentGitPushToBranch)
@@ -74,6 +75,7 @@ func registerRoutes(r *gin.Engine) {
7475
projectGroup.DELETE("/agentic-sessions/:sessionName/content-pod", handlers.DeleteContentPod)
7576
projectGroup.POST("/agentic-sessions/:sessionName/workflow", handlers.SelectWorkflow)
7677
projectGroup.GET("/agentic-sessions/:sessionName/workflow/metadata", handlers.GetWorkflowMetadata)
78+
projectGroup.GET("/agentic-sessions/:sessionName/workflow/results", handlers.GetWorkflowResults)
7779
projectGroup.POST("/agentic-sessions/:sessionName/repos", handlers.AddRepo)
7880
projectGroup.DELETE("/agentic-sessions/:sessionName/repos/:repoName", handlers.RemoveRepo)
7981

components/frontend/package-lock.json

Lines changed: 48 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"devDependencies": {
4343
"@eslint/eslintrc": "^3",
4444
"@tailwindcss/postcss": "^4",
45+
"@tailwindcss/typography": "^0.5.19",
4546
"@types/node": "^20",
4647
"@types/react": "^19",
4748
"@types/react-dom": "^19",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BACKEND_URL } from '@/lib/config';
2+
import { buildForwardHeadersAsync } from '@/lib/auth';
3+
4+
export async function GET(
5+
request: Request,
6+
{ params }: { params: Promise<{ name: string; sessionName: string }> },
7+
) {
8+
const { name, sessionName } = await params;
9+
const headers = await buildForwardHeadersAsync(request);
10+
const resp = await fetch(
11+
`${BACKEND_URL}/projects/${encodeURIComponent(name)}/agentic-sessions/${encodeURIComponent(sessionName)}/workflow/results`,
12+
{ headers }
13+
);
14+
const data = await resp.text();
15+
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } });
16+
}
17+

0 commit comments

Comments
 (0)