AgentConversationFilesFolder.tsx5.1 KBView on GitHub 'use client';
import { useState } from 'react';
import {
conversationFilePath,
conversationPath,
} from '@/modules/conversations/conversation-routes';
import { FileListRow, FileListRows } from '@/modules/files/components/list';
import type { FileEditor } from '@/modules/files/components/list';
import type { AgentOutputFile } from '@/modules/agents/types';
/**
* The agent's per-deal output, grouped BY DEAL.
*
* These documents live at `conversation/{id}/agent-{agentId}/…` — the deal's copy of the
* agent's folder, not the user's — so they are not rows of the tree above and cannot be:
* that tree is rooted in one scope, and its rename/move/delete all write to that scope.
* Putting them in it would offer three mutations that would each be refused, on rows the
* agent does not own.
*
* It used to be ONE folder called "In conversations" holding a flat run of files, which
* answered the wrong question. An agent's per-deal output is `overview` written forty
* times — once per deal — so a flat list is forty rows with the same name on them, and the
* only thing that tells them apart is the one field the row did not show. Grouping by deal
* makes the deal the row and the file the detail, which is the way round a person actually
* looks for these: "what did it write on Acme", never "show me every overview".
*
* The read is CAPPED and the list does not say so. "Showing the 8 most recent of 40" is an
* apology for a limit nobody asked about, in the one spot a reader is scanning for a deal
* name — and the fix it implies (see the other 32) is not on offer here anyway; the deal's
* own Files tab has them. The rows that are here are the recent ones, which is what a
* reverse-chronological list already means.
*
* A click still LEAVES: a file here is not opened in place, because the file is not here.
* It opens the deal on that file, through `conversationFilePath` — the one place the
* `?conversationId={id}/files/{docId}` grammar is written down. That is why these rows are
* links and the tree's are buttons.
*/
export function AgentConversationFilesFolder({
files,
editors,
}: {
files: AgentOutputFile[];
/** documentId → last editor, from `documents.lastEditors`. Absent renders "—". */
editors?: Map<string, FileEditor>;
}) {
/** Which deals are open, by id. Deals are closed by default — there can be dozens. */
const [open, setOpen] = useState<ReadonlySet<string>>(new Set());
if (files.length === 0) return null;
// Grouped in ARRIVAL order, which is `updatedAt` descending — so the deal the agent
// touched most recently is the first one you see, and re-sorting by name here would
// bury it alphabetically.
const byConversation = new Map<string, AgentOutputFile[]>();
for (const file of files) {
const key=[redacted] ?? '';
if (!key) continue;
const group = byConversation.get(key);
if (group) group.push(file);
else byConversation.set(key, [file]);
}
const toggle = (id: string) =>
setOpen((prev) => {
const next = new Set(prev);
if (!next.delete(id)) next.add(id);
return next;
});
return (
<FileListRows className="flex flex-col">
{[...byConversation].map(([conversationId, group]) => {
const expanded = open.has(conversationId);
return (
<div key=[redacted] className="flex flex-col">
<FileListRow
as="div"
// Under the "Conversations" heading, which is a folder row — so a deal is a
// folder inside it and its files are one step deeper again.
depth={1}
item={{
id: `conv-${conversationId}`,
// The deal's name, and its id only when the name is gone — a heading that
// reads `5f2a1c88-…` names nothing, but a row that is missing entirely
// hides files that do exist.
title: group[0]?.conversationName ?? conversationId,
kind: 'folder',
count: group.length,
}}
expandable
expanded={expanded}
onActivate={() => toggle(conversationId)}
testId={`agent-conversation-${conversationId}`}
/>
{expanded &&
group.map((file) => (
<FileListRow
key=[redacted]
as="link"
depth={2}
href={
file.conversationId
? conversationFilePath(file.conversationId, file.documentId)
: conversationPath(conversationId)
}
item={{
id: file.documentId,
title: file.name,
kind: file.documentType === 'table' ? 'table' : 'document',
editedBy: editors?.get(file.documentId) ?? null,
updatedAt: file.updatedAt,
}}
testId={`agent-in-conversation-${file.documentId}`}
/>
))}
</div>
);
})}
</FileListRows>
);
}