Skip to content

feat(app): CMP-46 add the private agent builder workspace - #62

Open
ripgrim wants to merge 1 commit into
rg/agent-builder-stack-03-foundationsfrom
rg/agent-builder-stack-04-workspace
Open

feat(app): CMP-46 add the private agent builder workspace#62
ripgrim wants to merge 1 commit into
rg/agent-builder-stack-03-foundationsfrom
rg/agent-builder-stack-04-workspace

Conversation

@ripgrim

@ripgrim ripgrim commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the private agent-builder workspace: durable builder chats, intent-aware composer, clarification flow, code and diff review, transcript rendering, sharing, navigation, and agent detail surfaces.

Why

Users need a complete workspace where the builder does the heavy lifting, preserves the conversation, and exposes the generated artifact before deployment.

Stack

Depends on #61 — CRM UI foundations.

  1. #67 — durable custom-agent domain
  2. #60 — sandboxed builder and runner runtimes
  3. #61 — CRM UI foundations
  4. #62 — private agent-builder workspace
  5. #63 — pre-deployment draft review
  6. #64 — builder presentation
  7. #65 — inline composer context

Verification

  • Full typecheck passes at this isolated slice
  • Lint passes
  • Builder-focused app tests pass

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
crm-agent Ready Ready Preview Aug 6, 2026 9:25pm
crm-api Ready Ready Preview Aug 6, 2026 9:25pm
crm-app Ready Ready Preview Aug 6, 2026 9:25pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 issues found across 49 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/components/agent-builder/agent-composer.tsx">

<violation number="1" location="apps/app/components/agent-builder/agent-composer.tsx:241">
P2: Selecting two different files with the same name, MIME type, and byte size silently drops the second attachment. Uploads need a unique client-side identity (preserved through rendering/state) rather than metadata-only deduplication.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/(agent-builder)/chat/page.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/(agent-builder)/chat/page.tsx:29">
P2: The home composer still issues fresh `builderResources` and Google status requests after the server has prefetched them, because `ChatHome` is not wrapped in its own `HydrateClient`; wrapping the returned `AgentBuilderHome` in `HydrateClient` (and importing it) would preserve the prefetch and avoid the duplicate request/loading on every initial navigation.</violation>
</file>

<file name="apps/app/components/crm/agent-panel.tsx">

<violation number="1" location="apps/app/components/crm/agent-panel.tsx:411">
P2: An unanswered clarification is now displayed twice: once as the new transcript “Follow-up” item and again as the fixed composer at the bottom. Keeping the pending question only in the composer (while retaining answered follow-ups in the transcript) would avoid the duplicate prompt.</violation>
</file>

<file name="apps/app/test/api-proxy-response.spec.ts">

<violation number="1" location="apps/app/test/api-proxy-response.spec.ts:38">
P3: The gzip test claims to verify header stripping, but passes an empty `new Headers()` as the output headers and never asserts the resulting headers. Feed the upstream's `content-encoding` through the same path (or pass headers with `content-encoding: gzip`) and assert the decoded response has no `content-encoding`, so a leak of the encoding header on a decoded body is actually caught.</violation>
</file>

<file name="apps/app/components/agent-builder/share-chat-dialog.tsx">

<violation number="1" location="apps/app/components/agent-builder/share-chat-dialog.tsx:45">
P1: Before `shareStatus` resolves, or when it fails, the dialog presents an actively shared chat as private and allows the user to choose that state without revoking the link. Gate the choices on a successful status result and provide an explicit loading/error state instead of treating missing data as `false`.</violation>

<violation number="2" location="apps/app/components/agent-builder/share-chat-dialog.tsx:143">
P1: Navigating from a shared chat to another chat can copy the first chat's link while the dialog is displaying the second chat. Scope or clear the token whenever `conversationId` changes so the new conversation must obtain its own share token.</violation>
</file>

<file name="packages/ui/src/components/shimmer.tsx">

<violation number="1" location="packages/ui/src/components/shimmer.tsx:33">
P3: The arbitrary-value class `[background-repeat:no-repeat,padding-box]` sets an invalid value: `padding-box` is not a valid `background-repeat` keyword, so browsers discard the whole declaration and the highlight layer falls back to the default `repeat`, letting the 250%-wide moving-gradient band tile across the text. Make the declaration valid, e.g. `[background-repeat:repeat,no-repeat]`, or drop the extraneous second value entirely.</violation>
</file>

<file name="apps/app/lib/agent-transcript.ts">

<violation number="1" location="apps/app/lib/agent-transcript.ts:349">
P2: Retrying after a failed turn remains marked as failed while the new turn is running, because this scan never clears the previous failure when a later `turn.started` arrives. Treat a newer turn start as the boundary (or associate failures with turn IDs) so the retry can show its live progress.</violation>
</file>

<file name="packages/ui/src/components/skeleton-swap.tsx">

<violation number="1" location="packages/ui/src/components/skeleton-swap.tsx:88">
P2: When the skeleton is displayed, the visually hidden real content remains keyboard-focusable, so users can tab to and select stale resource results while `resourceResults.isFetching` is true. Mark the real-content layer inert for the skeleton phase so its descendants are removed from interaction and accessibility navigation.</violation>
</file>

<file name="apps/app/components/agent-builder/agent-builder-shell.tsx">

<violation number="1" location="apps/app/components/agent-builder/agent-builder-shell.tsx:18">
P2: The desktop loading state shows an empty 213px sidebar instead of the skeleton because this boundary never suspends: `AgentBuilderSidebar` uses plain `useQuery` and the no-data shell path passes no `initialData`. Rendering `AgentBuilderSidebarFallback` when `sidebarData` is absent (or switching the sidebar queries to suspense) would give the streamed layout fallback a visible loading state.</violation>

<violation number="2" location="apps/app/components/agent-builder/agent-builder-shell.tsx:31">
P3: Screen readers receive the `Chats` heading without a loading state while this fallback is displayed; adding `aria-busy="true"` to the fallback aside would identify the sidebar as pending, consistent with the app’s other shell fallbacks.</violation>
</file>

<file name="apps/app/components/agent-builder/team-agent-detail.tsx">

<violation number="1" location="apps/app/components/agent-builder/team-agent-detail.tsx:115">
P2: Pausing or resuming an agent leaves the Activity tab stale because the shared success invalidation omits `agents.activity`. Including that query in `invalidate` would refresh the audit history immediately after the status change.</violation>

<violation number="2" location="apps/app/components/agent-builder/team-agent-detail.tsx:123">
P2: After a successful manual run, the Runs list updates but the displayed run count and Activity tab still omit the new run request because only the history query is invalidated. Invalidating the agent detail, list, and activity queries here would keep all surfaces consistent.</violation>

<violation number="3" location="apps/app/components/agent-builder/team-agent-detail.tsx:424">
P3: The Overview, Runs, and Activity controls expose no selected state to assistive technology even though they switch tab content. Adding proper tablist/tab semantics with `aria-selected` (or an equivalent pressed state) would make the active panel discoverable to screen-reader users.</violation>

<violation number="4" location="apps/app/components/agent-builder/team-agent-detail.tsx:508">
P3: The run outcome filter omits the supported Waiting for approval and Cancelled statuses, so selecting a specific outcome cannot cover all statuses shown in the list. Adding options for both statuses would make the filter complete.</violation>

<violation number="5" location="apps/app/components/agent-builder/team-agent-detail.tsx:527">
P3: Filtering runs changes their displayed Run numbers because the label uses the filtered array index rather than each run's position in the full history. Deriving the index from `runs` (and removing the now-unused map index) would preserve stable run numbers across filters.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

...trpc.conversations.shareStatus.queryOptions({ id: conversationId }),
enabled: open,
});
const shared = choice ?? status.data?.enabled ?? false;

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Before shareStatus resolves, or when it fails, the dialog presents an actively shared chat as private and allows the user to choose that state without revoking the link. Gate the choices on a successful status result and provide an explicit loading/error state instead of treating missing data as false.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/share-chat-dialog.tsx, line 45:

<comment>Before `shareStatus` resolves, or when it fails, the dialog presents an actively shared chat as private and allows the user to choose that state without revoking the link. Gate the choices on a successful status result and provide an explicit loading/error state instead of treating missing data as `false`.</comment>

<file context>
@@ -0,0 +1,261 @@
+		...trpc.conversations.shareStatus.queryOptions({ id: conversationId }),
+		enabled: open,
+	});
+	const shared = choice ?? status.data?.enabled ?? false;
+
+	const invalidate = () =>
</file context>
Fix with cubic

detail="Read-only"
disabled={createAction.pending || revokeAction.pending}
onSelect={() => {
if (!token) createAction.run();

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Navigating from a shared chat to another chat can copy the first chat's link while the dialog is displaying the second chat. Scope or clear the token whenever conversationId changes so the new conversation must obtain its own share token.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/share-chat-dialog.tsx, line 143:

<comment>Navigating from a shared chat to another chat can copy the first chat's link while the dialog is displaying the second chat. Scope or clear the token whenever `conversationId` changes so the new conversation must obtain its own share token.</comment>

<file context>
@@ -0,0 +1,261 @@
+						detail="Read-only"
+						disabled={createAction.pending || revokeAction.pending}
+						onSelect={() => {
+							if (!token) createAction.run();
+							setChoice(true);
+						}}
</file context>
Fix with cubic


function attachmentKey(attachment: ChatChipAttachment): string {
return (
attachment.id ?? `${attachment.name}:${attachment.type}:${attachment.size}`

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Selecting two different files with the same name, MIME type, and byte size silently drops the second attachment. Uploads need a unique client-side identity (preserved through rendering/state) rather than metadata-only deduplication.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/agent-composer.tsx, line 241:

<comment>Selecting two different files with the same name, MIME type, and byte size silently drops the second attachment. Uploads need a unique client-side identity (preserved through rendering/state) rather than metadata-only deduplication.</comment>

<file context>
@@ -0,0 +1,864 @@
+
+function attachmentKey(attachment: ChatChipAttachment): string {
+	return (
+		attachment.id ?? `${attachment.name}:${attachment.type}:${attachment.size}`
+	);
+}
</file context>
Fix with cubic

queryClient.prefetchQuery(trpc.google.status.queryOptions()),
]);

return <AgentBuilderHome name={session.user.name} />;

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The home composer still issues fresh builderResources and Google status requests after the server has prefetched them, because ChatHome is not wrapped in its own HydrateClient; wrapping the returned AgentBuilderHome in HydrateClient (and importing it) would preserve the prefetch and avoid the duplicate request/loading on every initial navigation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/(agent-builder)/chat/page.tsx, line 29:

<comment>The home composer still issues fresh `builderResources` and Google status requests after the server has prefetched them, because `ChatHome` is not wrapped in its own `HydrateClient`; wrapping the returned `AgentBuilderHome` in `HydrateClient` (and importing it) would preserve the prefetch and avoid the duplicate request/loading on every initial navigation.</comment>

<file context>
@@ -0,0 +1,38 @@
+		queryClient.prefetchQuery(trpc.google.status.queryOptions()),
+	]);
+
+	return <AgentBuilderHome name={session.user.name} />;
+}
+
</file context>
Fix with cubic

);
}

if (item.kind === "asked") {

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An unanswered clarification is now displayed twice: once as the new transcript “Follow-up” item and again as the fixed composer at the bottom. Keeping the pending question only in the composer (while retaining answered follow-ups in the transcript) would avoid the duplicate prompt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/agent-panel.tsx, line 411:

<comment>An unanswered clarification is now displayed twice: once as the new transcript “Follow-up” item and again as the fixed composer at the bottom. Keeping the pending question only in the composer (while retaining answered follow-ups in the transcript) would avoid the duplicate prompt.</comment>

<file context>
@@ -362,29 +387,42 @@ const SOURCE_ICONS: Record<Source["network"], CarbonIcon> = {
 		);
 	}
 
+	if (item.kind === "asked") {
+		return (
+			<div className="w-full max-w-sm border-ring/50 border-l-2 bg-muted/40 px-3 py-2.5">
</file context>
Fix with cubic

ease: "linear",
repeat: Number.POSITIVE_INFINITY,
}}
className={cn(

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The arbitrary-value class [background-repeat:no-repeat,padding-box] sets an invalid value: padding-box is not a valid background-repeat keyword, so browsers discard the whole declaration and the highlight layer falls back to the default repeat, letting the 250%-wide moving-gradient band tile across the text. Make the declaration valid, e.g. [background-repeat:repeat,no-repeat], or drop the extraneous second value entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/shimmer.tsx, line 33:

<comment>The arbitrary-value class `[background-repeat:no-repeat,padding-box]` sets an invalid value: `padding-box` is not a valid `background-repeat` keyword, so browsers discard the whole declaration and the highlight layer falls back to the default `repeat`, letting the 250%-wide moving-gradient band tile across the text. Make the declaration valid, e.g. `[background-repeat:repeat,no-repeat]`, or drop the extraneous second value entirely.</comment>

<file context>
@@ -0,0 +1,49 @@
+				ease: "linear",
+				repeat: Number.POSITIVE_INFINITY,
+			}}
+			className={cn(
+				"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
+				"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
</file context>
Fix with cubic


function AgentBuilderSidebarFallback() {
return (
<aside className="hidden w-[213px] flex-none flex-col border-r p-4 md:flex">

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Screen readers receive the Chats heading without a loading state while this fallback is displayed; adding aria-busy="true" to the fallback aside would identify the sidebar as pending, consistent with the app’s other shell fallbacks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/agent-builder-shell.tsx, line 31:

<comment>Screen readers receive the `Chats` heading without a loading state while this fallback is displayed; adding `aria-busy="true"` to the fallback aside would identify the sidebar as pending, consistent with the app’s other shell fallbacks.</comment>

<file context>
@@ -0,0 +1,42 @@
+
+function AgentBuilderSidebarFallback() {
+	return (
+		<aside className="hidden w-[213px] flex-none flex-col border-r p-4 md:flex">
+			<div className="flex h-7 items-center pl-2 font-medium text-xs">
+				Chats
</file context>
Fix with cubic

<option value="SUCCEEDED">Succeeded</option>
<option value="FAILED">Failed</option>
<option value="RUNNING">Running</option>
<option value="QUEUED">Queued</option>

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The run outcome filter omits the supported Waiting for approval and Cancelled statuses, so selecting a specific outcome cannot cover all statuses shown in the list. Adding options for both statuses would make the filter complete.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/team-agent-detail.tsx, line 508:

<comment>The run outcome filter omits the supported Waiting for approval and Cancelled statuses, so selecting a specific outcome cannot cover all statuses shown in the list. Adding options for both statuses would make the filter complete.</comment>

<file context>
@@ -0,0 +1,788 @@
+					<option value="SUCCEEDED">Succeeded</option>
+					<option value="FAILED">Failed</option>
+					<option value="RUNNING">Running</option>
+					<option value="QUEUED">Queued</option>
+				</select>
+			</div>
</file context>
Suggested change
<option value="QUEUED">Queued</option>
<option value="QUEUED">Queued</option>
<option value="WAITING_FOR_APPROVAL">Waiting for approval</option>
<option value="CANCELLED">Cancelled</option>
Fix with cubic

children: React.ReactNode;
}) {
return (
<button

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Overview, Runs, and Activity controls expose no selected state to assistive technology even though they switch tab content. Adding proper tablist/tab semantics with aria-selected (or an equivalent pressed state) would make the active panel discoverable to screen-reader users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/team-agent-detail.tsx, line 424:

<comment>The Overview, Runs, and Activity controls expose no selected state to assistive technology even though they switch tab content. Adding proper tablist/tab semantics with `aria-selected` (or an equivalent pressed state) would make the active panel discoverable to screen-reader users.</comment>

<file context>
@@ -0,0 +1,788 @@
+	children: React.ReactNode;
+}) {
+	return (
+		<button
+			type="button"
+			onClick={onClick}
</file context>
Fix with cubic

<span className="min-w-0 flex-1">
<span className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="font-semibold text-sm">
Run #{String(runs.length - index).padStart(3, "0")}

@cubic-dev-ai cubic-dev-ai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Filtering runs changes their displayed Run numbers because the label uses the filtered array index rather than each run's position in the full history. Deriving the index from runs (and removing the now-unused map index) would preserve stable run numbers across filters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/agent-builder/team-agent-detail.tsx, line 527:

<comment>Filtering runs changes their displayed Run numbers because the label uses the filtered array index rather than each run's position in the full history. Deriving the index from `runs` (and removing the now-unused map index) would preserve stable run numbers across filters.</comment>

<file context>
@@ -0,0 +1,788 @@
+						<span className="min-w-0 flex-1">
+							<span className="flex flex-wrap items-center gap-x-3 gap-y-1">
+								<span className="font-semibold text-sm">
+									Run #{String(runs.length - index).padStart(3, "0")}
+								</span>
+								<span
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant