Skip to content

feat: actionable AI pipeline suggestions with conversation UI#88

Merged
TerrifiedBug merged 26 commits intomainfrom
ai-agent-improvements-e78
Mar 11, 2026
Merged

feat: actionable AI pipeline suggestions with conversation UI#88
TerrifiedBug merged 26 commits intomainfrom
ai-agent-improvements-e78

Conversation

@TerrifiedBug
Copy link
Owner

Summary

  • Transforms AI pipeline review from raw text/YAML output into structured, individually-actionable suggestion cards users can selectively apply to their canvas
  • Adds persistent per-pipeline AI conversations with message history (new AiConversation + AiMessage Prisma models)
  • Implements 4 suggestion types as a discriminated union: modify config, add/remove components, and rewire connections
  • Rewrites the AI review dialog with a conversation thread, suggestion cards with checkboxes, batch select, Apply All, conflict detection, and markdown fallback
  • Adds audit logging for AI review and suggestion application actions

Changes

Data Layer: New AiConversation and AiMessage models with cascade delete on Pipeline, SetNull on User

Backend:

  • streamCompletion extended to accept conversation message history
  • Review system prompt rewritten to return structured JSON
  • New tRPC ai router (getConversation, startNewConversation, markSuggestionsApplied)
  • Streaming endpoint persists conversations, sends conversationId via SSE, writes audit logs

Client Logic:

  • Pure suggestion applier functions (config merge, node positioning, edge rewiring)
  • Client-side conflict detection between selected suggestions
  • Suggestion validator with componentKey reference checking and outdated detection
  • Flow store applySuggestions batch action with single undo snapshot

UI:

  • useAiConversation hook for conversation loading, streaming, and management
  • AiSuggestionCard with status/priority/type badges and conflict warnings
  • AiMessageBubble with selection state and Apply All / Apply Selected
  • AiPipelineDialog rewritten with conversation thread, New Conversation button, and graceful markdown fallback

Test plan

  • Generate mode still works unchanged (no regression)
  • Review mode returns structured JSON suggestions as cards
  • Suggestion cards render with correct status states (actionable, applied, outdated, invalid)
  • Batch select and Apply All work correctly
  • Undo reverses the entire applied batch in one step
  • Follow-up questions work with conversation history
  • New Conversation button clears thread and starts fresh
  • Audit logs are written for review and apply actions
  • Fallback to raw markdown works when AI returns non-JSON
  • Conflict warnings appear when selecting conflicting suggestions

@github-actions github-actions bot added documentation Improvements or additions to documentation feature and removed documentation Improvements or additions to documentation labels Mar 10, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR transforms the AI pipeline dialog from a raw text output into a structured, conversational suggestion system. It introduces persistent AiConversation / AiMessage models, a new ai tRPC router, a streaming endpoint that saves conversation history, and a full client-side suggestion lifecycle (apply, undo, conflict detection, outdated detection). The generate tab is untouched.

Key changes by layer:

  • Data layer: Two new Prisma models with correct cascade-delete semantics and composite indexes matching the access patterns
  • Backend: The streaming route correctly enforces team membership (EDITOR role) and pipeline-ownership before persisting any conversation data; audit logs are written for both review and apply actions
  • Client logic: suggestion-applier.ts, conflict-detector.ts, and suggestion-validator.ts are clean pure functions with solid coverage of all four suggestion types
  • State management: applySuggestions correctly pushes a single undo snapshot for the entire batch

Issue to address: markSuggestionsApplied performs a read-modify-write on the JSONB suggestions column without a transaction, which can silently drop another user's appliedAt markers in a concurrent team environment — important to fix before shipping the collaborative conversation feature.

Confidence Score: 3/5

  • Safe to merge in single-user scenarios; the markSuggestionsApplied race condition should be fixed before relying on applied-status tracking in shared team pipelines.
  • The core architecture, security model, and client-side logic are all well-implemented. The one correctness bug is a non-transactional read-modify-write on a JSONB column that can silently erase applied-suggestion markers when two team members apply suggestions concurrently. Given the PR explicitly targets collaborative team usage, this is worth fixing before the feature is considered complete.
  • src/server/routers/ai.ts — the markSuggestionsApplied mutation needs a transaction or an atomic JSON update to avoid the lost-update race.

Important Files Changed

Filename Overview
src/server/routers/ai.ts New tRPC AI router with proper withTeamAccess + withAudit on all procedures, but markSuggestionsApplied has a read-modify-write race condition on the JSONB suggestions column that can silently drop applied-status in concurrent multi-user scenarios.
src/app/api/ai/pipeline/route.ts Rewrites the streaming pipeline AI endpoint to persist conversations and audit log AI reviews. Team membership verification (EDITOR required) and pipeline-to-team ownership checks are both present and correct.
src/hooks/use-ai-conversation.ts New hook managing conversation loading, streaming, and state. Works correctly overall, but syncs two state slices via separate setState calls during render instead of a useEffect, which is a React anti-pattern that can cause two re-renders instead of one.
src/lib/ai/suggestion-applier.ts Pure, immutable suggestion applier functions for all four suggestion types. Logic is correct: config deep-merge, node insertion with edge rewiring, reconnect on removal, and edge add/remove. No issues found.
src/stores/flow-store.ts Adds applySuggestions batch action that iterates suggestions, applies each via the pure applier, and pushes a single undo snapshot. Undo semantics are correct — only one snapshot is pushed regardless of how many suggestions are applied.
prisma/schema.prisma Adds AiConversation and AiMessage models with correct cascade deletes (Pipeline → Conversation → Message) and SetNull on User deletion. Indexes on (pipelineId, createdAt) and (conversationId, createdAt) are appropriate for the access patterns.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant D as AiPipelineDialog
    participant H as useAiConversation
    participant API as /api/ai/pipeline (SSE)
    participant DB as PostgreSQL
    participant AI as AI Provider

    U->>D: Opens Review tab
    D->>H: useAiConversation({ pipelineId })
    H->>+DB: trpc.ai.getConversation (VIEWER)
    DB-->>-H: AiConversation + AiMessages
    H->>D: messages[], conversationId

    U->>D: Submits prompt
    D->>H: sendReview(prompt)
    H->>+API: POST { pipelineId, conversationId, prompt }
    API->>DB: Verify pipeline ownership & team membership
    API->>DB: AiMessage.create (role: user)
    API->>DB: AiMessage.findMany (last 10 for history)
    API-->>H: SSE: { conversationId }
    API->>AI: streamCompletion(systemPrompt + history)
    AI-->>API: token stream
    API-->>H: SSE: { token } (streamed)
    API->>DB: AiMessage.create (role: assistant + suggestions JSON)
    API->>DB: writeAuditLog(pipeline.ai_review)
    API-->>-H: SSE: { done }

    H->>DB: fetchQuery getConversation (staleTime: 0)
    DB-->>H: Updated messages with real IDs
    H->>D: setMessages(refetched)

    U->>D: Clicks "Apply Selected"
    D->>H: onApplySelected(messageId, suggestions)
    H->>D: applySuggestions(suggestions) via flow-store
    D->>DB: trpc.ai.markSuggestionsApplied (EDITOR)
    DB-->>D: { applied: N }
Loading

Comments Outside Diff (2)

  1. src/server/routers/ai.ts, line 54-80 (link)

    Lost update race condition in markSuggestionsApplied

    This mutation uses a read-modify-write pattern without any transaction or optimistic locking. In a collaborative scenario — which is explicitly supported ("visible to all team members with access") — two editors applying different suggestions simultaneously will cause one user's applied status to be silently overwritten.

    Concrete sequence:

    1. User A reads suggestions = [{ id: "s1", ... }, { id: "s2", ... }]
    2. User B reads the same array
    3. User A writes [{ id: "s1", appliedAt: "...", ... }, { id: "s2", ... }]
    4. User B writes [{ id: "s1", ... }, { id: "s2", appliedAt: "...", ... }]s1's appliedAt is gone

    After both writes, the conversation shows only s2 as applied even though both were applied to their respective canvases. The affected user won't see their suggestion marked as Applied.

    The fix is to use a Prisma $queryRaw with a JSON merge (jsonb_set), or wrap the read-modify-write in a serializable transaction, or use a $executeRaw UPDATE WHERE suggestions @> ... approach. Alternatively, move appliedAt to a separate AiSuggestionApplied join table.

  2. src/hooks/use-ai-conversation.ts, line 51-64 (link)

    Two separate setState calls during render

    Calling setConversationId and setMessages as separate statements during the component's render phase causes React to schedule two distinct re-renders instead of one. Between those two re-renders there is a window where conversationId is set but messages is still [], which could cause a momentary flash or double-render.

    The React-approved approach for initializing state from an external data source is a single useEffect:

    useEffect(() => {
      if (
        loadedConversation &&
        !conversationId &&
        messages.length === 0 &&
        !isStreaming &&
        !isNewConversationRef.current
      ) {
        setConversationId(loadedConversation.id);
        setMessages(
          loadedConversation.messages.map((m) => ({
            id: m.id,
            role: m.role as "user" | "assistant",
            content: m.content,
            suggestions: m.suggestions as unknown as AiSuggestion[] | undefined,
            pipelineYaml: m.pipelineYaml,
            createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : String(m.createdAt),
            createdBy: m.createdBy,
          })),
        );
      }
    }, [loadedConversation]); // eslint-disable-line react-hooks/exhaustive-deps

    React 18 batches the two setter calls in most contexts, so this likely doesn't manifest as a visible bug in practice — but the useEffect form is safer and more aligned with the React model.

Last reviewed commit: 26ec9b9

@TerrifiedBug
Copy link
Owner Author

@greptile review

- Verify pipelineId belongs to teamId in streaming route
- Verify conversationId belongs to pipelineId when reusing conversations
- Add pipeline ownership check in markSuggestionsApplied (conversationId↔pipelineId)
- Use TRPCError instead of raw Error for consistent error handling
- Add withAudit middleware to startNewConversation mutation
…tion tracking

- Only push undo snapshot when at least one suggestion is applied (prevents
  ghost Ctrl+Z entries when all suggestions fail)
- Replace manual writeAuditLog with withAudit middleware in markSuggestionsApplied
  to match project conventions
- Await prisma.aiMessage.create before sending done:true SSE event,
  eliminating the race where client refetch returns stale data
- Replace temp message IDs with real server-persisted IDs by refetching
  after streaming completes, so markSuggestionsApplied finds the row
- Guard markSuggestionsApplied against temp- prefixed IDs as a safety net
- Clear TanStack Query cache in startNewConversation to prevent the sync
  guard from repopulating the old conversation from cache
…-e78

# Conflicts:
#	agent/internal/agent/agent.go
#	agent/internal/agent/poller.go
#	src/server/services/ws-auth.ts
- Add isNewConversationRef to block the render-time sync guard from
  repopulating old messages after React Query background refetches
- Resolve dot-notation keys in modify_config changes (e.g. "codec.fields")
  into proper nested objects instead of creating literal flat keys
@TerrifiedBug
Copy link
Owner Author

@greptile fixed

[Critical] Missing team membership check — False positive. The membership check already exists at
route.ts:54-67: prisma.teamMember.findUnique({ userId_teamId }) validates the user belongs to body.teamId
before any pipeline access. Non-members get 403.

[Important] "New Conversation" refetch loop — Fixed. The previous removeQueries fix was incomplete because
useQuery (still mounted with enabled: !!pipelineId) immediately refetches, and the sync guard repopulates
old messages. Added an isNewConversationRef that blocks the sync guard until the user sends their first
message in the new conversation.

[Important] Shallow config merge — Fixed. Added setAtPath() helper that recursively resolves dot-notation
keys like "codec.except_fields" into proper nested objects, instead of the previous { ...existingConfig,
...changes } which created literal flat keys.

The read-modify-write on the JSONB suggestions column was not atomic,
so concurrent team members marking different suggestions could silently
overwrite each other's appliedAt markers.
@TerrifiedBug TerrifiedBug merged commit c12ad0a into main Mar 11, 2026
5 checks passed
@TerrifiedBug TerrifiedBug deleted the ai-agent-improvements-e78 branch March 11, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant