Skip to content

Commit 9a8bed4

Browse files
authored
Merge pull request #100 from Gkrumbach07/bug-fixes
Bug fixes
2 parents ee8d562 + 68e6b5c commit 9a8bed4

File tree

13 files changed

+399
-286
lines changed

13 files changed

+399
-286
lines changed

components/backend/handlers.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,14 @@ func createSession(c *gin.Context) {
545545
},
546546
}
547547

548+
// Only include paths if a workspacePath was provided
549+
if strings.TrimSpace(req.WorkspacePath) != "" {
550+
spec := session["spec"].(map[string]interface{})
551+
spec["paths"] = map[string]interface{}{
552+
"workspace": req.WorkspacePath,
553+
}
554+
}
555+
548556
// Optional environment variables passthrough (always, independent of git config presence)
549557
if len(req.EnvironmentVariables) > 0 {
550558
spec := session["spec"].(map[string]interface{})
@@ -2904,32 +2912,50 @@ func initSpecKitInWorkspace(c *gin.Context, project, workspaceRoot string) error
29042912
return err
29052913
}
29062914
// Extract files
2915+
total := len(zr.File)
2916+
var filesWritten, skippedDirs, openErrors, readErrors, writeErrors int
2917+
log.Printf("initSpecKitInWorkspace: extracting spec-kit template: %d entries", total)
29072918
for _, f := range zr.File {
29082919
if f.FileInfo().IsDir() {
2920+
skippedDirs++
2921+
log.Printf("spec-kit: skipping directory: %s", f.Name)
29092922
continue
29102923
}
29112924
rc, err := f.Open()
29122925
if err != nil {
2926+
openErrors++
2927+
log.Printf("spec-kit: open failed: %s: %v", f.Name, err)
29132928
continue
29142929
}
29152930
b, err := io.ReadAll(rc)
29162931
rc.Close()
29172932
if err != nil {
2933+
readErrors++
2934+
log.Printf("spec-kit: read failed: %s: %v", f.Name, err)
29182935
continue
29192936
}
29202937
// Normalize path: strip any leading directory components to place at workspace root
29212938
rel := f.Name
2939+
origRel := rel
29222940
rel = strings.TrimLeft(rel, "./")
29232941
// Ensure we do not write outside workspace
29242942
rel = strings.ReplaceAll(rel, "\\", "/")
29252943
for strings.Contains(rel, "../") {
29262944
rel = strings.ReplaceAll(rel, "../", "")
29272945
}
2946+
if rel != origRel {
2947+
log.Printf("spec-kit: normalized path %q -> %q", origRel, rel)
2948+
}
29282949
target := filepath.Join(workspaceRoot, rel)
29292950
if err := writeProjectContentFile(c, project, target, b); err != nil {
2951+
writeErrors++
29302952
log.Printf("write spec-kit file failed: %s: %v", target, err)
2953+
} else {
2954+
filesWritten++
2955+
log.Printf("spec-kit: wrote %s (%d bytes)", target, len(b))
29312956
}
29322957
}
2958+
log.Printf("initSpecKitInWorkspace: extraction summary: written=%d, skipped_dirs=%d, open_errors=%d, read_errors=%d, write_errors=%d", filesWritten, skippedDirs, openErrors, readErrors, writeErrors)
29332959
return nil
29342960
}
29352961

@@ -3133,18 +3159,10 @@ func listProjectRFEWorkflowSessions(c *gin.Context) {
31333159
return
31343160
}
31353161

3136-
// Build a thin DTO for UI
3162+
// Return full session objects for UI
31373163
sessions := make([]map[string]interface{}, 0, len(list.Items))
31383164
for _, item := range list.Items {
3139-
meta, _ := item.Object["metadata"].(map[string]interface{})
3140-
status, _ := item.Object["status"].(map[string]interface{})
3141-
name, _ := meta["name"].(string)
3142-
phase, _ := status["phase"].(string)
3143-
sessions = append(sessions, map[string]interface{}{
3144-
"name": name,
3145-
"phase": phase,
3146-
"labels": meta["labels"],
3147-
})
3165+
sessions = append(sessions, item.Object)
31483166
}
31493167
c.JSON(http.StatusOK, gin.H{"sessions": sessions})
31503168
}

components/backend/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ type CreateAgenticSessionRequest struct {
308308
LLMSettings *LLMSettings `json:"llmSettings,omitempty"`
309309
Timeout *int `json:"timeout,omitempty"`
310310
Interactive *bool `json:"interactive,omitempty"`
311+
WorkspacePath string `json:"workspacePath,omitempty"`
311312
GitConfig *GitConfig `json:"gitConfig,omitempty"`
312313
UserContext *UserContext `json:"userContext,omitempty"`
313314
BotAccount *BotAccountRef `json:"botAccount,omitempty"`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { BACKEND_URL } from '@/lib/config'
2+
import { buildForwardHeadersAsync } from '@/lib/auth'
3+
4+
// GET /api/projects/[name]/agents/[persona]/markdown - Fetch agent markdown
5+
export async function GET(
6+
request: Request,
7+
{ params }: { params: Promise<{ name: string; persona: string }> },
8+
) {
9+
try {
10+
const { name, persona } = await params
11+
const headers = await buildForwardHeadersAsync(request)
12+
const resp = await fetch(`${BACKEND_URL}/projects/${encodeURIComponent(name)}/agents/${encodeURIComponent(persona)}/markdown`, { headers })
13+
const text = await resp.text()
14+
// Markdown; backend likely sets text/markdown, but ensure passthrough if absent
15+
const contentType = resp.headers.get('Content-Type') || 'text/markdown; charset=utf-8'
16+
return new Response(text, { status: resp.status, headers: { 'Content-Type': contentType } })
17+
} catch (error) {
18+
console.error('Error fetching agent markdown:', error)
19+
return new Response('# Error fetching agent markdown', { status: 500, headers: { 'Content-Type': 'text/markdown; charset=utf-8' } })
20+
}
21+
}
22+
23+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { BACKEND_URL } from '@/lib/config'
2+
import { buildForwardHeadersAsync } from '@/lib/auth'
3+
4+
// GET /api/projects/[name]/agents - List agents for a project
5+
export async function GET(
6+
request: Request,
7+
{ params }: { params: Promise<{ name: string }> },
8+
) {
9+
try {
10+
const { name } = await params
11+
const headers = await buildForwardHeadersAsync(request)
12+
const resp = await fetch(`${BACKEND_URL}/projects/${encodeURIComponent(name)}/agents`, { headers })
13+
const data = await resp.text()
14+
return new Response(data, { status: resp.status, headers: { 'Content-Type': 'application/json' } })
15+
} catch (error) {
16+
console.error('Error listing agents:', error)
17+
return Response.json({ error: 'Failed to list agents' }, { status: 500 })
18+
}
19+
}
20+
21+

components/frontend/src/app/projects/[name]/rfe/[id]/page.tsx

Lines changed: 48 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1010
import { getApiUrl } from "@/lib/config";
1111
import { formatDistanceToNow } from "date-fns";
1212
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
13-
import { RFEWorkflow, WorkflowPhase } from "@/types/agentic-session";
13+
import { AgenticSession, CreateAgenticSessionRequest, RFEWorkflow, WorkflowPhase } from "@/types/agentic-session";
1414
import { WORKFLOW_PHASE_LABELS } from "@/lib/agents";
15-
import { ArrowLeft, Edit, Play, Loader2, RefreshCw, FolderTree } from "lucide-react";
15+
import { ArrowLeft, Play, Loader2, FolderTree, Plus } from "lucide-react";
1616
import { FileTree, type FileTreeNode } from "@/components/file-tree";
1717

1818
function phaseProgress(w: RFEWorkflow, phase: WorkflowPhase) {
@@ -32,7 +32,7 @@ export default function ProjectRFEDetailPage() {
3232
const [error, setError] = useState<string | null>(null);
3333
const [advancing, setAdvancing] = useState(false);
3434
const [startingPhase, setStartingPhase] = useState<WorkflowPhase | null>(null);
35-
const [rfeSessions, setRfeSessions] = useState<Array<{ name: string; phase?: string; labels?: Record<string, unknown> }>>([]);
35+
const [rfeSessions, setRfeSessions] = useState<AgenticSession[]>([]);
3636
const [sessionsLoading, setSessionsLoading] = useState(false);
3737
const [hasWorkspace, setHasWorkspace] = useState<boolean | null>(null);
3838
const [wsTree, setWsTree] = useState<FileTreeNode[]>([]);
@@ -119,6 +119,18 @@ export default function ProjectRFEDetailPage() {
119119
})();
120120
}, [project, id, listWsPath, probeWorkspaceAndPhase]);
121121

122+
const updateChildrenByPath = useCallback((nodes: FileTreeNode[], targetPath: string, children: FileTreeNode[]): FileTreeNode[] => {
123+
return nodes.map((n) => {
124+
if (n.path === targetPath) {
125+
return { ...n, children };
126+
}
127+
if (n.type === "folder" && n.children && n.children.length > 0) {
128+
return { ...n, children: updateChildrenByPath(n.children, targetPath, children) };
129+
}
130+
return n;
131+
});
132+
}, []);
133+
122134
const onWsToggle = useCallback(async (node: FileTreeNode) => {
123135
if (node.type !== "folder") return;
124136
const items = await listWsPath(node.path);
@@ -129,8 +141,8 @@ export default function ProjectRFEDetailPage() {
129141
expanded: false,
130142
sizeKb: typeof it.size === "number" ? it.size / 1024 : undefined,
131143
}));
132-
setWsTree(prev => prev.map(n => n.path === node.path ? { ...n, children } : n));
133-
}, [listWsPath]);
144+
setWsTree(prev => updateChildrenByPath(prev, node.path, children));
145+
}, [listWsPath, updateChildrenByPath]);
134146

135147
const onWsSelect = useCallback(async (node: FileTreeNode) => {
136148
if (node.type !== "file") return;
@@ -204,11 +216,6 @@ export default function ProjectRFEDetailPage() {
204216
<p className="text-muted-foreground mt-1">{workflow.description}</p>
205217
</div>
206218
</div>
207-
<div className="flex gap-2">
208-
<Link href={`/projects/${encodeURIComponent(project)}/rfe/${encodeURIComponent(id)}/edit`}>
209-
<Button variant="outline" size="sm"><Edit className="mr-2 h-4 w-4" />Edit</Button>
210-
</Link>
211-
</div>
212219
</div>
213220

214221
<div className="grid gap-6 md:grid-cols-3">
@@ -286,29 +293,37 @@ export default function ProjectRFEDetailPage() {
286293
return "specs/tasks.md";
287294
})();
288295
const exists = phase === "specify" ? specExists : phase === "plan" ? planExists : tasksExists;
289-
const sessionForPhase = rfeSessions.find(s => (s.labels as any)?.["rfe-phase"] === phase);
290-
const running = (sessionForPhase?.phase || "").toLowerCase() === "running";
291-
const completed = (sessionForPhase?.phase || "").toLowerCase() === "completed";
296+
const sessionForPhase = rfeSessions.find(s => (s.metadata.labels)?.["rfe-phase"] === phase);
297+
292298
const prerequisitesMet = phase === "specify" ? true : phase === "plan" ? specExists : (specExists && planExists);
299+
const sessionDisplay = ((sessionForPhase as any)?.spec?.displayName) || sessionForPhase?.metadata.name;
293300
return (
294301
<div key={phase} className="p-4 rounded-lg border flex items-center justify-between">
295-
<div className="flex items-center gap-3">
296-
<Badge variant="outline">{WORKFLOW_PHASE_LABELS[phase]}</Badge>
297-
<span className="text-sm text-muted-foreground">{expected}</span>
302+
<div className="flex flex-col gap-1">
303+
<div className="flex items-center gap-3">
304+
<Badge variant="outline">{WORKFLOW_PHASE_LABELS[phase]}</Badge>
305+
<span className="text-sm text-muted-foreground">{expected}</span>
306+
</div>
307+
{sessionForPhase && (
308+
<div className="flex items-center gap-2">
309+
<Link href={`/projects/${encodeURIComponent(project)}/sessions/${encodeURIComponent(sessionForPhase.metadata.name)}`}>
310+
<Button variant="link" size="sm" className="px-0 h-auto">{sessionDisplay}</Button>
311+
</Link>
312+
{sessionForPhase?.status?.phase && <Badge variant="outline">{sessionForPhase.status.phase}</Badge>}
313+
</div>
314+
)}
298315
</div>
299316
<div className="flex items-center gap-3">
300317
<Badge variant={exists ? "outline" : "secondary"}>{exists ? "Exists" : (prerequisitesMet ? "Missing" : "Blocked")}</Badge>
301-
{running && <Badge variant="outline">Running</Badge>}
302-
{completed && <Badge variant="outline">Completed</Badge>}
303-
{!exists && !running && (
318+
{!exists && sessionForPhase?.status?.phase !== "Running" && (
304319
<Button size="sm" onClick={async () => {
305320
try {
306321
setStartingPhase(phase);
307-
const payload = {
322+
const payload: CreateAgenticSessionRequest = {
308323
prompt: `/${phase} ${workflow.description}`,
309324
displayName: `${workflow.title} - ${phase}`,
310-
interactive: false,
311-
sharedWorkspace: workflowWorkspace,
325+
interactive: false,
326+
workspacePath: workflowWorkspace,
312327
environmentVariables: {
313328
WORKFLOW_PHASE: phase,
314329
PARENT_RFE: workflow.id,
@@ -357,10 +372,12 @@ export default function ProjectRFEDetailPage() {
357372
<CardTitle>Agentic Sessions ({rfeSessions.length})</CardTitle>
358373
<CardDescription>Sessions scoped to this RFE</CardDescription>
359374
</div>
360-
<Button variant="outline" size="sm" onClick={loadSessions} disabled={sessionsLoading}>
361-
<RefreshCw className={`w-4 h-4 mr-2 ${sessionsLoading ? "animate-spin" : ""}`} />
362-
Refresh
363-
</Button>
375+
<Link href={`/projects/${encodeURIComponent(project)}/sessions/new?workspacePath=${encodeURIComponent(workflowWorkspace)}&rfeWorkflow=${encodeURIComponent(workflow.id)}`}>
376+
<Button variant="default" size="sm">
377+
<Plus className="w-4 h-4 mr-2" />
378+
Create Session
379+
</Button>
380+
</Link>
364381
</div>
365382
</CardHeader>
366383
<CardContent>
@@ -380,14 +397,14 @@ export default function ProjectRFEDetailPage() {
380397
{rfeSessions.length === 0 ? (
381398
<TableRow><TableCell colSpan={6} className="py-6 text-center text-muted-foreground">No agent sessions yet</TableCell></TableRow>
382399
) : (
383-
rfeSessions.map((s: any) => {
384-
const labels = (s.labels || {}) as Record<string, unknown>;
385-
const name = s.name;
400+
rfeSessions.map((s) => {
401+
const labels = (s.metadata.labels || {}) as Record<string, unknown>;
402+
const name = s.metadata.name;
386403
const display = s.spec?.displayName || name;
387404
const rfePhase = typeof labels["rfe-phase"] === "string" ? String(labels["rfe-phase"]) : '';
388405
const model = s.spec?.llmSettings?.model;
389406
const created = s.metadata?.creationTimestamp ? formatDistanceToNow(new Date(s.metadata.creationTimestamp), { addSuffix: true }) : '';
390-
const cost = s.status?.cost;
407+
const cost = s.status?.total_cost_usd;
391408
return (
392409
<TableRow key={name}>
393410
<TableCell className="font-medium min-w-[180px]">
@@ -397,7 +414,7 @@ export default function ProjectRFEDetailPage() {
397414
</Link>
398415
</TableCell>
399416
<TableCell>{WORKFLOW_PHASE_LABELS[rfePhase as WorkflowPhase] || rfePhase || '—'}</TableCell>
400-
<TableCell><span className="text-sm">{s.phase || 'Pending'}</span></TableCell>
417+
<TableCell><span className="text-sm">{s.status?.phase || 'Pending'}</span></TableCell>
401418
<TableCell className="hidden md:table-cell"><span className="text-sm text-gray-600 truncate max-w-[160px] block">{model || '—'}</span></TableCell>
402419
<TableCell className="hidden lg:table-cell">{created || <span className="text-gray-400"></span>}</TableCell>
403420
<TableCell className="hidden xl:table-cell">{cost ? <span className="text-sm font-mono">${cost.toFixed?.(4) ?? cost}</span> : <span className="text-gray-400"></span>}</TableCell>
@@ -442,68 +459,6 @@ export default function ProjectRFEDetailPage() {
442459
</TabsContent>
443460
</Tabs>
444461

445-
<Card>
446-
<CardHeader>
447-
<div className="flex items-center justify-between">
448-
<div>
449-
<CardTitle>Agentic Sessions ({rfeSessions.length})</CardTitle>
450-
<CardDescription>Sessions scoped to this RFE</CardDescription>
451-
</div>
452-
<Button variant="outline" size="sm" onClick={loadSessions} disabled={sessionsLoading}>
453-
<RefreshCw className={`w-4 h-4 mr-2 ${sessionsLoading ? "animate-spin" : ""}`} />
454-
Refresh
455-
</Button>
456-
</div>
457-
</CardHeader>
458-
<CardContent>
459-
<div className="overflow-x-auto">
460-
<Table>
461-
<TableHeader>
462-
<TableRow>
463-
<TableHead className="min-w-[220px]">Name</TableHead>
464-
<TableHead>Stage</TableHead>
465-
<TableHead>Status</TableHead>
466-
<TableHead className="hidden md:table-cell">Model</TableHead>
467-
<TableHead className="hidden lg:table-cell">Created</TableHead>
468-
<TableHead className="hidden xl:table-cell">Cost</TableHead>
469-
</TableRow>
470-
</TableHeader>
471-
<TableBody>
472-
{rfeSessions.length === 0 ? (
473-
<TableRow><TableCell colSpan={6} className="py-6 text-center text-muted-foreground">No agent sessions yet</TableCell></TableRow>
474-
) : (
475-
rfeSessions.map((s: any) => {
476-
const labels = (s.labels || {}) as Record<string, unknown>;
477-
const name = s.name;
478-
const display = s.spec?.displayName || name;
479-
const rfePhase = typeof labels["rfe-phase"] === "string" ? String(labels["rfe-phase"]) : '';
480-
const model = s.spec?.llmSettings?.model;
481-
const created = s.metadata?.creationTimestamp ? formatDistanceToNow(new Date(s.metadata.creationTimestamp), { addSuffix: true }) : '';
482-
const cost = s.status?.cost;
483-
return (
484-
<TableRow key={name}>
485-
<TableCell className="font-medium min-w-[180px]">
486-
<Link href={`/projects/${encodeURIComponent(project)}/sessions/${encodeURIComponent(name)}`} className="text-blue-600 hover:underline hover:text-blue-800 transition-colors block">
487-
<div className="font-medium">{display}</div>
488-
{display !== name && (<div className="text-xs text-gray-500">{name}</div>)}
489-
</Link>
490-
</TableCell>
491-
<TableCell>{WORKFLOW_PHASE_LABELS[rfePhase as WorkflowPhase] || rfePhase || '—'}</TableCell>
492-
<TableCell><span className="text-sm">{s.phase || 'Pending'}</span></TableCell>
493-
<TableCell className="hidden md:table-cell"><span className="text-sm text-gray-600 truncate max-w-[160px] block">{model || '—'}</span></TableCell>
494-
<TableCell className="hidden lg:table-cell">{created || <span className="text-gray-400"></span>}</TableCell>
495-
<TableCell className="hidden xl:table-cell">{cost ? <span className="text-sm font-mono">${cost.toFixed?.(4) ?? cost}</span> : <span className="text-gray-400"></span>}</TableCell>
496-
</TableRow>
497-
);
498-
})
499-
)}
500-
</TableBody>
501-
</Table>
502-
</div>
503-
</CardContent>
504-
</Card>
505-
506-
{/* Artifacts grid omitted; use Workspace tab */}
507462
</div>
508463
</div>
509464
);

0 commit comments

Comments
 (0)