feat(frontend): viewer humain fil-par-paire en lecture seule (LS7)
Frontend pur, lecture seule, consomme read_conversation_page (LS6) ; aucun changement backend. - adapters : conversation.ts (gateway) + conversationNormalization.ts. - features/conversations : useConversationThread, ConversationViewer, index. - domain (types LS7), ports (port + Gateways.conversation), adapters/index, mock (+ clé inventaire). - features/workstate : ProjectWorkStatePanel prop onOpenConversation. - features/projects : ProjectsView swap viewer. - tests (QA, verts) : conversationNormalization, mock/conversationGateway, useConversationThread, ConversationViewer, ProjectsView.ls7. tsc 0, vitest 443/443 (+36). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,8 @@ import { useProjectWorkState } from "./useProjectWorkState";
|
||||
|
||||
export interface ProjectWorkStatePanelProps {
|
||||
projectId: string;
|
||||
/** Opens the read-only transcript viewer for a conversation pair (LS7). */
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}
|
||||
|
||||
interface WorkStatePanelErrorBoundaryProps {
|
||||
@ -133,8 +135,10 @@ async function copyToClipboard(text: string): Promise<void> {
|
||||
|
||||
function ConversationSummaryLine({
|
||||
summary,
|
||||
onOpenConversation,
|
||||
}: {
|
||||
summary: ConversationWorkSummary;
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const text = summaryText(summary);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@ -153,7 +157,18 @@ function ConversationSummaryLine({
|
||||
>
|
||||
{summaryLabel(summary.status)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 break-words">{text}</span>
|
||||
{onOpenConversation ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`open conversation ${summary.conversationId}`}
|
||||
className="min-w-0 flex-1 break-words text-left hover:text-content hover:underline"
|
||||
onClick={() => onOpenConversation(summary.conversationId)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
) : (
|
||||
<span className="min-w-0 flex-1 break-words">{text}</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded px-1 py-0.5 text-[11px] text-muted hover:bg-raised hover:text-content"
|
||||
@ -211,9 +226,11 @@ function ConversationSummaryLine({
|
||||
function TicketRow({
|
||||
ticket,
|
||||
summary,
|
||||
onOpenConversation,
|
||||
}: {
|
||||
ticket: AgentTicketState;
|
||||
summary?: ConversationWorkSummary;
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const truncated = ticket.taskLen > ticket.taskPreview.length;
|
||||
return (
|
||||
@ -251,7 +268,12 @@ function TicketRow({
|
||||
{shortTicket(ticket.ticketId)}
|
||||
</code>
|
||||
</div>
|
||||
{summary && <ConversationSummaryLine summary={summary} />}
|
||||
{summary && (
|
||||
<ConversationSummaryLine
|
||||
summary={summary}
|
||||
onOpenConversation={onOpenConversation}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@ -263,6 +285,7 @@ function AgentRow({
|
||||
layout,
|
||||
projectId,
|
||||
onRefresh,
|
||||
onOpenConversation,
|
||||
}: {
|
||||
agent: AgentWorkState;
|
||||
conversations: Map<string, ConversationWorkSummary>;
|
||||
@ -270,6 +293,7 @@ function AgentRow({
|
||||
layout: LayoutViewModel;
|
||||
projectId: string;
|
||||
onRefresh: () => Promise<void>;
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const { agent: agentGateway } = useGateways();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
@ -431,6 +455,7 @@ function AgentRow({
|
||||
key={ticket.ticketId}
|
||||
ticket={ticket}
|
||||
summary={conversations.get(ticket.conversationId)}
|
||||
onOpenConversation={onOpenConversation}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@ -439,7 +464,10 @@ function AgentRow({
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectWorkStatePanelContent({ projectId }: ProjectWorkStatePanelProps) {
|
||||
function ProjectWorkStatePanelContent({
|
||||
projectId,
|
||||
onOpenConversation,
|
||||
}: ProjectWorkStatePanelProps) {
|
||||
const vm = useProjectWorkState(projectId);
|
||||
const layoutVm = useLayout(projectId);
|
||||
const agents = vm.state?.agents ?? [];
|
||||
@ -491,6 +519,7 @@ function ProjectWorkStatePanelContent({ projectId }: ProjectWorkStatePanelProps)
|
||||
layout={layoutVm}
|
||||
projectId={projectId}
|
||||
onRefresh={vm.refresh}
|
||||
onOpenConversation={onOpenConversation}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user