feat(app): CMP-46 add the private agent builder workspace - #62
Conversation
44b1b26 to
270805e
Compare
270805e to
db1abac
Compare
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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>
| detail="Read-only" | ||
| disabled={createAction.pending || revokeAction.pending} | ||
| onSelect={() => { | ||
| if (!token) createAction.run(); |
There was a problem hiding this comment.
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>
|
|
||
| function attachmentKey(attachment: ChatChipAttachment): string { | ||
| return ( | ||
| attachment.id ?? `${attachment.name}:${attachment.type}:${attachment.size}` |
There was a problem hiding this comment.
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>
| queryClient.prefetchQuery(trpc.google.status.queryOptions()), | ||
| ]); | ||
|
|
||
| return <AgentBuilderHome name={session.user.name} />; |
There was a problem hiding this comment.
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>
| ); | ||
| } | ||
|
|
||
| if (item.kind === "asked") { |
There was a problem hiding this comment.
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>
| ease: "linear", | ||
| repeat: Number.POSITIVE_INFINITY, | ||
| }} | ||
| className={cn( |
There was a problem hiding this comment.
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>
|
|
||
| function AgentBuilderSidebarFallback() { | ||
| return ( | ||
| <aside className="hidden w-[213px] flex-none flex-col border-r p-4 md:flex"> |
There was a problem hiding this comment.
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>
| <option value="SUCCEEDED">Succeeded</option> | ||
| <option value="FAILED">Failed</option> | ||
| <option value="RUNNING">Running</option> | ||
| <option value="QUEUED">Queued</option> |
There was a problem hiding this comment.
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>
| <option value="QUEUED">Queued</option> | |
| <option value="QUEUED">Queued</option> | |
| <option value="WAITING_FOR_APPROVAL">Waiting for approval</option> | |
| <option value="CANCELLED">Cancelled</option> |
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <button |
There was a problem hiding this comment.
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>
| <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")} |
There was a problem hiding this comment.
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>
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.
Verification