Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/demo-capture-ux-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agentweaver": patch
---

Fix demo capture UX: SlidePanel sticky footer, OutcomePlanPanel footer hoist, CoordinatorRunPage wiring, TeamPage Promise.all cold-start race. Capture plan: beat 1.3 heading wait + 60s timeout, beat 2.2 plan panel open via chip click + 120s Confirm timeout, beat 2.5 followNewPage 60s timeout.
89 changes: 55 additions & 34 deletions apps/web/src/components/OutcomePlanPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@fluentui/react-icons';
import { AgentStepList } from './ui/agentic';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import type { RunStreamEvent, StreamStatus } from '../api/sse';
import type { OutcomeSpec, OutcomeSpecStatus } from '../api/types';
import { isTerminalRunStatus, normalizeRunStatus } from '../utils/runStatus';
Expand Down Expand Up @@ -226,9 +227,16 @@ interface OutcomePlanPanelProps {
onReconnect?: () => void;
onClarifyPlan?: () => void;
clarificationSent?: boolean;
/**
* When provided, the confirm/clarify action row (and the Independent task promotion
* field above it) is reported to the caller instead of being rendered inline. This lets a
* host such as SlidePanel pin it outside a scrollable body so it's always visible without
* scrolling. Called with `null` whenever the plan isn't awaiting confirmation.
*/
onFooterChange?: (node: ReactNode) => void;
}

export function OutcomePlanPanel({ runId, events, streamStatus, runStatus, onCollapse, onReconnect, onClarifyPlan, clarificationSent = false }: OutcomePlanPanelProps) {
export function OutcomePlanPanel({ runId, events, streamStatus, runStatus, onCollapse, onReconnect, onClarifyPlan, clarificationSent = false, onFooterChange }: OutcomePlanPanelProps) {
const styles = useStyles();

const [specFromApi, setSpecFromApi] = useState<OutcomeSpec | null>(null);
Expand Down Expand Up @@ -485,6 +493,51 @@ export function OutcomePlanPanel({ runId, events, streamStatus, runStatus, onCol
setReviseOpen(true);
};

// The confirm/clarify action row, plus the Independent task promotion field above it.
// Rendered inline by default, but when a host supplies `onFooterChange` (e.g. SlidePanel
// pinning it outside a scrollable body) it's reported via that callback instead so it stays
// visible without scrolling.
const footerContent = awaiting ? (
<>
<Field
label="Independent task promotion"
hint="Optional: allow the coordinator to split genuinely separate deliverables into standalone backlog tasks. Leave off to keep all work inline in this run."
>
<Checkbox
checked={allowTaskPromotion}
disabled={acting || revising || runInterrupted || runTerminal}
label="Allow standalone backlog tasks for independent deliverables"
onChange={(_, data) => setAllowTaskPromotion(Boolean(data.checked))}
/>
</Field>
<div role="group" className={styles.actionRow}>
<Button
appearance="primary"
icon={<CheckmarkCircleRegular />}
disabled={acting || revising || runInterrupted || runTerminal}
onClick={() => void handleConfirm()}
>
{acting ? 'Confirming plan...' : 'Confirm plan'}
</Button>
<Button
appearance="secondary"
icon={<EditRegular />}
disabled={acting || revising || runInterrupted || runTerminal}
onClick={openRevise}
>
Clarify plan
</Button>
</div>
</>
) : null;

useEffect(() => {
if (!onFooterChange) return;
onFooterChange(footerContent);
return () => onFooterChange(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onFooterChange, awaiting, allowTaskPromotion, acting, revising, runInterrupted, runTerminal]);

return (
<div className={styles.panel}>
<div className={styles.header}>
Expand Down Expand Up @@ -596,39 +649,7 @@ export function OutcomePlanPanel({ runId, events, streamStatus, runStatus, onCol
</MessageBar>
)}

{awaiting && (
<>
<Field
label="Independent task promotion"
hint="Optional: allow the coordinator to split genuinely separate deliverables into standalone backlog tasks. Leave off to keep all work inline in this run."
>
<Checkbox
checked={allowTaskPromotion}
disabled={acting || revising || runInterrupted || runTerminal}
label="Allow standalone backlog tasks for independent deliverables"
onChange={(_, data) => setAllowTaskPromotion(Boolean(data.checked))}
/>
</Field>
<div role="group" className={styles.actionRow}>
<Button
appearance="primary"
icon={<CheckmarkCircleRegular />}
disabled={acting || revising || runInterrupted || runTerminal}
onClick={() => void handleConfirm()}
>
{acting ? 'Confirming plan...' : 'Confirm plan'}
</Button>
<Button
appearance="secondary"
icon={<EditRegular />}
disabled={acting || revising || runInterrupted || runTerminal}
onClick={openRevise}
>
Clarify plan
</Button>
</div>
</>
)}
{!onFooterChange && footerContent}


<Dialog open={reviseOpen} onOpenChange={(_, d) => { setReviseOpen(d.open); if (!d.open) { setAnswers([]); setExtraFeedback(''); } }}>
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/components/SlidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ const useStyles = makeStyles({
padding: 0,
gap: 0,
},
footer: {
flexShrink: 0,
borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
},
});

const focusableSelector = [
Expand Down Expand Up @@ -138,6 +146,9 @@ export interface SlidePanelProps {
flushBody?: boolean;
bodyClassName?: string;
variant?: 'default' | 'copilotDock';
/** Optional content pinned below the scrollable body (e.g. action buttons that must stay
* visible without scrolling). Renders nothing when absent. */
footer?: ReactNode;
children: ReactNode;
}

Expand All @@ -152,6 +163,7 @@ export function SlidePanel({
flushBody = false,
bodyClassName,
variant = 'default',
footer,
children,
}: SlidePanelProps) {
const styles = useStyles();
Expand Down Expand Up @@ -261,6 +273,11 @@ export function SlidePanel({
<div className={mergeClasses(styles.body, flushBody && styles.bodyFlush, bodyClassName)}>
{open || keepMounted ? children : null}
</div>
{footer != null && (
<div className={styles.footer}>
{footer}
</div>
)}
</div>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/pages/CoordinatorRunPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3371,6 +3371,9 @@ export function CoordinatorRunPage() {
// ---------------------------------------------------------------------------

const [planPanelOpen, setPlanPanelOpen] = useState(false);
// Confirm/clarify action row for the Outcome plan panel, reported by OutcomePlanPanel so it
// can be pinned outside the SlidePanel's scrollable body (always visible, no scrolling).
const [planFooter, setPlanFooter] = useState<ReactNode>(null);
const [artifactsPanelOpen, setArtifactsPanelOpen] = useState(false);
// Files chip opens the produced-files browser.
const [filesPanelOpen, setFilesPanelOpen] = useState(false);
Expand Down Expand Up @@ -4671,6 +4674,7 @@ export function CoordinatorRunPage() {
onClose={() => setPlanPanelOpen(false)}
title="Outcome plan"
width="min(880px, 96vw)"
footer={planFooter}
>
<OutcomePlanPanel
runId={runId}
Expand All @@ -4680,6 +4684,7 @@ export function CoordinatorRunPage() {
onCollapse={() => setPlanPanelOpen(false)}
onReconnect={reconnectStream}
onClarifyPlan={() => { setPlanPanelOpen(false); focusOutcomePlanComposer(); }}
onFooterChange={setPlanFooter}
/>
</SlidePanel>
)}
Expand Down
52 changes: 33 additions & 19 deletions apps/web/src/pages/TeamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,29 +761,43 @@ export function TeamPage() {
useEffect(() => {
if (!projectId) return;
let cancelled = false;
Promise.all([
apiClient.getTeam(projectId).catch((err) => {
if (err instanceof ApiError && err.status === 404) return null;
throw err;
}),
apiClient.getTemplates().catch(() => [] as TeamTemplateDto[]),
apiClient.getProject(projectId).catch(() => null as Project | null),
])
.then(([t, s, p]) => {
if (!cancelled) {
setTeam(t);
setScenarios(s);
setProject(p);
}
setTeam(null); // eslint-disable-line react-hooks/set-state-in-effect -- reset stale team/project state when projectId changes
setProject(null);
setScenarios([]);
setLoading(true);
setError(null);

void apiClient.getTeam(projectId)
.then((t) => {
if (!cancelled) setTeam(t);
})
.catch((err) => {
if (!cancelled) setError(
err instanceof ApiError
? `API error ${err.status}: ${err.body}`
: err instanceof Error ? err.message : String(err),
);
if (!cancelled && !(err instanceof ApiError && err.status === 404)) {
setError(
err instanceof ApiError
? `API error ${err.status}: ${err.body}`
: err instanceof Error ? err.message : String(err),
);
}
})
.finally(() => { if (!cancelled) setLoading(false); });

void apiClient.getTemplates()
.then((s) => {
if (!cancelled) setScenarios(s);
})
.catch(() => {
if (!cancelled) setScenarios([]);
});

void apiClient.getProject(projectId)
.then((p) => {
if (!cancelled) setProject(p);
})
.catch(() => {
if (!cancelled) setProject(null);
});

return () => { cancelled = true; };
}, [projectId]);

Expand Down
6 changes: 5 additions & 1 deletion scripts/azure/tests/deploy-render.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,13 @@ test("writeOverlay() + kubectl kustomize builds cleanly and every resource resol
/name: NODE_OPTIONS\s*\n\s*value: --max-old-space-size=1024[\s\S]*?name: agentweaver-agent-host[\s\S]*?resources:\s*\n\s*limits:\s*\n\s*cpu: 800m\s*\n\s*ephemeral-storage: 4Gi\s*\n\s*memory: 2Gi\s*\n\s*requests:\s*\n\s*cpu: 300m\s*\n\s*ephemeral-storage: 1Gi\s*\n\s*memory: 1Gi/,
"AgentHost must pass the preview Node heap cap and retain its explicit resource reservation",
);
// Pre-existing drift (unrelated to this branch): dev's PR #931 ("raise agentweaver-exec memory
// limit 2Gi→4Gi to prevent preview server OOM") bumped the exec container's memory request/limit
// to 2Gi/4Gi in k8s/base/sandbox-template-agenthost.yaml without syncing this assertion. Updating
// the expectation here to match the shipped template rather than touching production config.
assert.match(
sandboxTemplate,
/name: agentweaver-exec[\s\S]*?resources:\s*\n\s*limits:\s*\n\s*cpu: 1200m\s*\n\s*ephemeral-storage: 4Gi\s*\n\s*memory: 2Gi\s*\n\s*requests:\s*\n\s*cpu: 700m\s*\n\s*ephemeral-storage: 1Gi\s*\n\s*memory: 1Gi/,
/name: agentweaver-exec[\s\S]*?resources:\s*\n\s*limits:\s*\n\s*cpu: 1200m\s*\n\s*ephemeral-storage: 4Gi\s*\n\s*memory: 4Gi\s*\n\s*requests:\s*\n\s*cpu: 700m\s*\n\s*ephemeral-storage: 1Gi\s*\n\s*memory: 2Gi/,
"The executor that runs previews must retain explicit resource reservation and limits",
);
const mcpDeployment = manifestForFilename(docs, "mcp-deployment.yaml");
Expand Down
Loading
Loading