diff --git a/backend/app/services/thread_service.py b/backend/app/services/thread_service.py
index c504928..772dd5b 100644
--- a/backend/app/services/thread_service.py
+++ b/backend/app/services/thread_service.py
@@ -5,6 +5,7 @@
from typing import Any, Dict, List, Optional
from uuid import UUID
+from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from app.models import Comment, ContextType, Project, Thread, User
diff --git a/frontend/app/projects/[projectId]/features/[featureId]/FeatureDetailClient.tsx b/frontend/app/projects/[projectId]/features/[featureId]/FeatureDetailClient.tsx
index 2bdcce0..fde20c0 100644
--- a/frontend/app/projects/[projectId]/features/[featureId]/FeatureDetailClient.tsx
+++ b/frontend/app/projects/[projectId]/features/[featureId]/FeatureDetailClient.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useCallback, useRef } from "react";
+import { useEffect, useState, useCallback, useRef, useMemo } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { apiClient } from "@/lib/api/client";
@@ -77,6 +77,7 @@ import { CopyableModuleKey } from "@/components/features/CopyableModuleKey";
import { GenerationProgressCard } from "@/components/brainstorming/GenerationProgressCard";
import { DecisionSummarizerStatus } from "@/components/DecisionSummarizerStatus";
import { useNav } from "@/lib/contexts/NavContext";
+import { useSidePanel } from "@/lib/contexts/SidePanelContext";
import { useProjectPermissions } from "@/lib/hooks/useProjectPermissions";
import { buildProjectUrl, buildPhaseUrl, buildDiscussionUrl } from "@/lib/url";
@@ -155,6 +156,7 @@ export default function FeatureDetailClient() {
const router = useRouter();
const { user } = useAuth();
const { currentProject } = useNav();
+ const { setSidePanelContent } = useSidePanel();
const searchParams = useSearchParams();
const projectId = params.projectId as string;
const featureId = params.featureId as string;
@@ -546,6 +548,71 @@ export default function FeatureDetailClient() {
setRefreshKey((k) => k + 1); // Refresh content viewers
}, []);
+ // Side panel: inject right sidebar content
+ const sidePanelContentNode = useMemo(
+ () => (
+
+
+
+ {selectedImplementation?.is_complete && selectedImplementation?.completion_summary && (
+
+
+
+
+ Completion Summary
+
+
+
+
+ {selectedImplementation.completion_summary}
+
+ {selectedImplementation.completed_at && (
+
+
+ {new Date(selectedImplementation.completed_at).toLocaleString(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })}
+
+ )}
+
+
+ )}
+
+ ),
+ [
+ featureId,
+ selectedImplementationId,
+ handleImplementationSelect,
+ refreshKey,
+ user?.org_id,
+ currentProject?.id,
+ threadId,
+ selectedImplementation,
+ ],
+ );
+
+ useEffect(() => {
+ setSidePanelContent(sidePanelContentNode);
+ }, [setSidePanelContent, sidePanelContentNode]);
+
+ // Clear side panel on unmount
+ useEffect(() => {
+ return () => setSidePanelContent(null);
+ }, [setSidePanelContent]);
+
// Handle new implementation created
const handleImplementationCreated = useCallback((impl: ImplementationListItem) => {
setImplementations((prev) => [...prev, impl]);
@@ -757,53 +824,52 @@ export default function FeatureDetailClient() {
>
)}
-
- {/* Left: Badges */}
-
- {/* Completion Status */}
- {(() => {
- const completionConfig = COMPLETION_CONFIG[feature.completion_status || "pending"];
- const CompletionIcon = completionConfig.icon;
- return (
-
-
- {completionConfig.label}
-
- );
- })()}
-
- {/* Priority */}
-
- {PRIORITY_CONFIG[feature.priority].label}
-
-
- {/* Provenance */}
-
- {PROVENANCE_BADGES[feature.provenance].label}
-
-
- {/* Status (only show if archived) */}
- {isArchived && (
+
+ {/* Completion Status */}
+ {(() => {
+ const completionConfig = COMPLETION_CONFIG[feature.completion_status || "pending"];
+ const CompletionIcon = completionConfig.icon;
+ return (
- {STATUS_BADGES[feature.status].label}
+
+ {completionConfig.label}
- )}
-
+ );
+ })()}
+
+ {/* Priority */}
+
+ {PRIORITY_CONFIG[feature.priority].label}
+
+
+ {/* Provenance */}
+
+ {PROVENANCE_BADGES[feature.provenance].label}
+
+
+ {/* Status (only show if archived) */}
+ {isArchived && (
+
+ {STATUS_BADGES[feature.status].label}
+
+ )}
+
- {/* Right: Stage Progress */}
- {/* When an implementation is selected, use its content status instead of feature-level */}
+ {/* Stage Progress - own row */}
+ {/* When an implementation is selected, use its content status instead of feature-level */}
+
-
+
Conversations
@@ -965,205 +1031,154 @@ export default function FeatureDetailClient() {
)}
- {/* Two-column layout: Tab content on left, Completion Summary on right */}
-
- {/* Left column: Tab content */}
-
-
- {feature.source_project_chat_id &&
- feature.source_project_chat_short_id &&
- currentProject && (
-
-
- Created from Project Chat:
-
- {feature.source_project_chat_title || "Discussion"}
-
-
- )}
-
+
+ {feature.source_project_chat_id &&
+ feature.source_project_chat_short_id &&
+ currentProject && (
+
+
+ Created from Project Chat:
+
+ {feature.source_project_chat_title || "Discussion"}
+
+
+ )}
+
+ {/* Conversation Panel */}
+
+
- {/* Conversation Panel */}
-
-
setRefreshKey((k) => k + 1)}
- highlightedItemId={highlightedItemId}
- targetMessageSequence={targetMessageSequence}
- aboveCommentInput={(() => {
- // Button visibility is controlled by the server via decision summarizer
- if (!hasConversation) return undefined;
-
- // If summarizer is running, show status instead of button
- if (isSummarizerRunning) {
- return (
-
-
-
- );
- }
-
- // Show button only when server says to (after decision summarizer completes)
- if (!showCreateImplementationButton) return undefined;
-
+ activeTab={activeTab}
+ onGenerate={handleContentGenerated}
+ onThreadLoaded={handleThreadLoaded}
+ onImplementationNameUpdated={() => setRefreshKey((k) => k + 1)}
+ highlightedItemId={highlightedItemId}
+ targetMessageSequence={targetMessageSequence}
+ aboveCommentInput={(() => {
+ // Button visibility is controlled by the server via decision summarizer
+ if (!hasConversation) return undefined;
+
+ // If summarizer is running, show status instead of button
+ if (isSummarizerRunning) {
return (
- 0}
- suggestedName={suggestedImplementationName}
- canCreate={canCreateFeatures}
- onCreated={handleImplementationCreated}
- />
+
);
- })()}
- />
-
-
-
-
- handleGenerateContent("spec")}
- onCancelClick={handleCancelGeneration}
- isCancelling={isCancellingGeneration}
- descriptionImages={feature.description_image_attachments}
- implementationContent={selectedImplementation?.spec_text}
- />
-
-
-
- handleGenerateContent("prompt_plan")}
- onCancelClick={handleCancelGeneration}
- isCancelling={isCancellingGeneration}
- descriptionImages={feature.description_image_attachments}
- implementationContent={selectedImplementation?.prompt_plan_text}
+ }
+
+ // Show button only when server says to (after decision summarizer completes)
+ if (!showCreateImplementationButton) return undefined;
+
+ return (
+
+ 0}
+ suggestedName={suggestedImplementationName}
+ canCreate={canCreateFeatures}
+ onCreated={handleImplementationCreated}
+ />
+
+ );
+ })()}
/>
-
-
-
-
-
-
-
-
-
-
+
+
- {/* Right column: Sidebar (sticky) */}
-
- {/* Implementation Selector Card */}
-
+ handleGenerateContent("spec")}
+ onCancelClick={handleCancelGeneration}
+ isCancelling={isCancellingGeneration}
+ descriptionImages={feature.description_image_attachments}
+ implementationContent={selectedImplementation?.spec_text}
/>
+
- {/* Feature Sidebar - Decisions, Engagement & Readiness */}
-
+ handleGenerateContent("prompt_plan")}
+ onCancelClick={handleCancelGeneration}
+ isCancelling={isCancellingGeneration}
+ descriptionImages={feature.description_image_attachments}
+ implementationContent={selectedImplementation?.prompt_plan_text}
/>
+
- {/* Completion Summary */}
- {selectedImplementation?.is_complete && selectedImplementation?.completion_summary && (
-
-
-
-
- Completion Summary
-
-
-
-
- {selectedImplementation.completion_summary}
-
- {selectedImplementation.completed_at && (
-
-
- {new Date(selectedImplementation.completed_at).toLocaleString(undefined, {
- dateStyle: "medium",
- timeStyle: "short",
- })}
-
- )}
-
-
- )}
-
+
+
+
+
+
+
+