diff --git a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
index 0efef480..3c2ded6c 100644
--- a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
+++ b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
@@ -1,31 +1,21 @@
import { useMemo, useState } from "react";
-import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui";
+import { Button, Icon, Segmented } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
-import type {
- AgentRecord,
- EmployeeRecord,
- FileAccessGrantRecord,
- FileObjectRecord,
-} from "../../../shared/domain";
import type { PageProps } from "./types";
-import { activeOrgId, formatBytes, formatRelative } from "./types";
-
-type ViewMode = "grid" | "list";
-type Section =
- | { kind: "all" }
- | { kind: "recent" }
- | { kind: "pinned" }
- | { kind: "department"; value: string }
- | { kind: "agent"; value: string };
-
-const FILE_COLOR_MAP: Record = {
- doc: { color: "#5e6ad2", label: "DOC" },
- sheet: { color: "#2f9e58", label: "XLS" },
- md: { color: "#52525b", label: "MD" },
- pdf: { color: "#d23b3b", label: "PDF" },
- img: { color: "#c87d12", label: "IMG" },
- folder: { color: "#a1a1aa", label: "" },
-};
+import { activeOrgId, formatBytes } from "./types";
+import { FilesTree } from "./files/FilesTree";
+import { FilesTable } from "./files/FilesTable";
+import { FilesGrid, PinnedCard } from "./files/FilesGrid";
+import {
+ collectAgentFiles,
+ collectDepartments,
+ fileGrantCount,
+ filterFiles,
+ labelFor,
+ resolveOwnerName,
+ type Section,
+ type ViewMode,
+} from "./files/file-helpers";
export function FilesPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
@@ -77,436 +67,47 @@ export function FilesPage({ snapshot, actions }: PageProps) {
- {pinnedFiles.length > 0 && section.kind === "all" && (
- <>
-
Pinned
-
- {pinnedFiles.slice(0, 8).map((file) => (
-
void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))}
- />
- ))}
-
- >
- )}
+ {pinnedFiles.length > 0 && section.kind === "all" && (
+ <>
+
Pinned
+
+ {pinnedFiles.slice(0, 8).map((file) => (
+
void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))}
+ />
+ ))}
+
+ >
+ )}
-
-
{labelFor(section)}
-
Modified ↓
-
+
+
{labelFor(section)}
+
Modified ↓
+
- {view === "list" ? (
-
void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
- />
- ) : (
- void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
- />
- )}
-
-
-
- );
-}
-
-function FilesTree({
- files,
- pinnedFiles,
- section,
- departments,
- filesByAgent,
- onChange,
-}: {
- files: FileObjectRecord[];
- pinnedFiles: FileObjectRecord[];
- section: Section;
- departments: Array<{ name: string; count: number }>;
- filesByAgent: Array<{ agent: AgentRecord; count: number }>;
- onChange: (next: Section) => void;
-}) {
- const recent = files.filter((file) => isRecent(file.createdAt));
- return (
-
- );
-}
-
-function TreeItem({
- icon,
- label,
- count,
- active,
- onClick,
-}: {
- icon: string;
- label: string;
- count: number;
- active: boolean;
- onClick: () => void;
-}) {
- return (
-
- );
-}
-
-function FilesTable({
- files,
- grants,
- employees,
- agents,
- onTogglePin,
-}: {
- files: FileObjectRecord[];
- grants: FileAccessGrantRecord[];
- employees: EmployeeRecord[];
- agents: AgentRecord[];
- onTogglePin: (file: FileObjectRecord) => void;
-}) {
- return (
-
-
-
Name
Size
Owner
Access
Modified
-
- {files.map((file, index) => (
-
onTogglePin(file)}
- />
- ))}
-
- );
-}
-
-function FilesGrid({
- files,
- grants,
- employees,
- agents,
- onTogglePin,
-}: {
- files: FileObjectRecord[];
- grants: FileAccessGrantRecord[];
- employees: EmployeeRecord[];
- agents: AgentRecord[];
- onTogglePin: (file: FileObjectRecord) => void;
-}) {
- return (
-
- {files.map((file) => (
-
onTogglePin(file)}
- />
- ))}
-
- );
-}
-
-function FileRow({
- file,
- owner,
- grantCount,
- last,
- onTogglePin,
-}: {
- file: FileObjectRecord;
- owner: string;
- grantCount: number;
- last: boolean;
- onTogglePin: () => void;
-}) {
- return (
-
-
-
-
- {file.name}
-
- sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
-
+ )}
- {file.pinned &&
Pinned}
-
-
{formatBytes(file.sizeBytes)}
-
-
-
{file.scanState === "approved" ? "Clean" : "Blocked"}
-
{grantCount}
-
-
{formatRelative(file.updatedAt ?? file.createdAt)}
-
-
);
}
-
-function PinnedCard({
- file,
- ownerName,
- grantCount,
- onUnpin,
-}: {
- file: FileObjectRecord;
- ownerName: string;
- grantCount: number;
- onUnpin: () => void;
-}) {
- return (
-
-
-
-
-
-
- {file.name}
-
-
- sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
-
-
-
{file.scanState === "approved" ? "Clean" : "Blocked"}
-
{grantCount}
-
-
-
-
-
- {formatRelative(file.updatedAt ?? file.createdAt)} · {ownerName.split(" ")[0]}
-
-
-
- );
-}
-
-function ScanLabelStrip({ file }: { file: FileObjectRecord }) {
- const labels = parseScanLabels(file).slice(0, 2);
- if (labels.length === 0) return null;
- return (
-
- {labels.map((label) => (
-
{label}
- ))}
-
- );
-}
-
-function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) {
- const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc;
- if (!map) throw new Error("File color map default missing.");
- if (kind === "folder") {
- return (
-
-
-
- );
- }
- const label = map.label || kind.slice(0, 3).toUpperCase();
- return (
-
- {label}
-
- );
-}
-
-function collectDepartments(agents: AgentRecord[]): Array<{ name: string; count: number }> {
- const seen = new Map();
- for (const agent of agents) {
- if (!agent.department) continue;
- seen.set(agent.department, (seen.get(agent.department) ?? 0) + 1);
- }
- return [...seen.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name));
-}
-
-function collectAgentFiles(files: FileObjectRecord[], agents: AgentRecord[]): Array<{ agent: AgentRecord; count: number }> {
- const out: Array<{ agent: AgentRecord; count: number }> = [];
- for (const agent of agents) {
- const count = files.filter((file) => file.ownerType === "agent" && file.ownerId === agent.id).length;
- if (count > 0) out.push({ agent, count });
- }
- return out.sort((a, b) => b.count - a.count);
-}
-
-function filterFiles(files: FileObjectRecord[], section: Section, agents: AgentRecord[]): FileObjectRecord[] {
- if (section.kind === "all") return files;
- if (section.kind === "pinned") return files.filter((file) => file.pinned);
- if (section.kind === "recent") return files.filter((file) => isRecent(file.createdAt));
- if (section.kind === "agent") return files.filter((file) => file.ownerType === "agent" && file.ownerId === section.value);
- if (section.kind === "department") {
- const departmentAgents = new Set(agents.filter((agent) => agent.department === section.value).map((agent) => agent.id));
- return files.filter((file) => file.ownerType === "agent" && file.ownerId !== null && departmentAgents.has(file.ownerId));
- }
- return files;
-}
-
-function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number {
- return grants.filter((grant) => grant.fileId === fileId).length;
-}
-
-function parseScanLabels(file: FileObjectRecord): string[] {
- const parsed: unknown = JSON.parse(file.scanLabelsJson);
- if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) {
- throw new Error("File scan labels are invalid.");
- }
- return parsed;
-}
-
-function shortChecksum(value: string): string {
- if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid.");
- return value.slice(0, 8);
-}
-
-function shortObjectRef(value: string): string {
- const parts = value.split(/[\\/]/).filter(Boolean);
- if (parts.length === 0) throw new Error("File object reference is invalid.");
- return parts.slice(-2).join("/");
-}
-
-function isRecent(iso: string): boolean {
- const sevenDays = 7 * 24 * 60 * 60 * 1000;
- return Date.now() - new Date(iso).getTime() <= sevenDays;
-}
-
-function isAgentOwner(file: FileObjectRecord): boolean {
- return file.ownerType === "agent";
-}
-
-function resolveOwnerName(file: FileObjectRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string {
- if (file.ownerType === "employee" && file.ownerId) {
- const employee = employees.find((item) => item.id === file.ownerId);
- if (employee) return employee.displayName;
- }
- if (file.ownerType === "agent" && file.ownerId) {
- const agent = agents.find((item) => item.id === file.ownerId);
- if (agent) return agent.name;
- }
- return "System";
-}
-
-function labelFor(section: Section): string {
- if (section.kind === "all") return "All files";
- if (section.kind === "recent") return "Recent";
- if (section.kind === "pinned") return "Pinned";
- if (section.kind === "agent") return "By agent";
- return section.value;
-}
diff --git a/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx b/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx
new file mode 100644
index 00000000..5f0cfb9b
--- /dev/null
+++ b/AgentCompany-New/src/renderer/src/pages/files/FileVisuals.tsx
@@ -0,0 +1,47 @@
+import { Icon, Pill } from "../../shared/ui";
+import type { FileObjectRecord } from "../../../../shared/domain";
+import { FILE_COLOR_MAP, parseScanLabels } from "./file-helpers";
+
+export function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) {
+ const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc;
+ if (!map) throw new Error("File color map default missing.");
+ if (kind === "folder") {
+ return (
+
+
+
+ );
+ }
+ const label = map.label || kind.slice(0, 3).toUpperCase();
+ return (
+
+ {label}
+
+ );
+}
+
+export function ScanLabelStrip({ file }: { file: FileObjectRecord }) {
+ const labels = parseScanLabels(file).slice(0, 2);
+ if (labels.length === 0) return null;
+ return (
+
+ {labels.map((label) => (
+
{label}
+ ))}
+
+ );
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx
new file mode 100644
index 00000000..27de60cf
--- /dev/null
+++ b/AgentCompany-New/src/renderer/src/pages/files/FilesGrid.tsx
@@ -0,0 +1,94 @@
+import { Avatar, Icon, Pill } from "../../shared/ui";
+import type {
+ AgentRecord,
+ EmployeeRecord,
+ FileAccessGrantRecord,
+ FileObjectRecord,
+} from "../../../../shared/domain";
+import { formatRelative } from "../types";
+import { FileIcon, ScanLabelStrip } from "./FileVisuals";
+import { fileGrantCount, isAgentOwner, resolveOwnerName, shortChecksum, shortObjectRef } from "./file-helpers";
+
+export function FilesGrid({
+ files,
+ grants,
+ employees,
+ agents,
+ onTogglePin,
+}: {
+ files: FileObjectRecord[];
+ grants: FileAccessGrantRecord[];
+ employees: EmployeeRecord[];
+ agents: AgentRecord[];
+ onTogglePin: (file: FileObjectRecord) => void;
+}) {
+ return (
+
+ {files.map((file) => (
+
onTogglePin(file)}
+ />
+ ))}
+
+ );
+}
+
+export function PinnedCard({
+ file,
+ ownerName,
+ grantCount,
+ onUnpin,
+}: {
+ file: FileObjectRecord;
+ ownerName: string;
+ grantCount: number;
+ onUnpin: () => void;
+}) {
+ return (
+
+
+
+
+
+
+ {file.name}
+
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
+
+
+
{file.scanState === "approved" ? "Clean" : "Blocked"}
+
{grantCount}
+
+
+
+
+
+ {formatRelative(file.updatedAt ?? file.createdAt)} · {ownerName.split(" ")[0]}
+
+
+
+ );
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx
new file mode 100644
index 00000000..b418eb6b
--- /dev/null
+++ b/AgentCompany-New/src/renderer/src/pages/files/FilesTable.tsx
@@ -0,0 +1,89 @@
+import { Avatar, IconButton, Pill } from "../../shared/ui";
+import type {
+ AgentRecord,
+ EmployeeRecord,
+ FileAccessGrantRecord,
+ FileObjectRecord,
+} from "../../../../shared/domain";
+import { formatBytes, formatRelative } from "../types";
+import { FileIcon } from "./FileVisuals";
+import { fileGrantCount, isAgentOwner, resolveOwnerName, shortChecksum, shortObjectRef } from "./file-helpers";
+
+const ROW_COLUMNS = "minmax(0, 2.7fr) minmax(70px, 0.7fr) minmax(0, 1.2fr) minmax(94px, 0.8fr) minmax(88px, 0.8fr) 56px";
+
+export function FilesTable({
+ files,
+ grants,
+ employees,
+ agents,
+ onTogglePin,
+}: {
+ files: FileObjectRecord[];
+ grants: FileAccessGrantRecord[];
+ employees: EmployeeRecord[];
+ agents: AgentRecord[];
+ onTogglePin: (file: FileObjectRecord) => void;
+}) {
+ return (
+
+
+
Name
Size
Owner
Access
Modified
+
+ {files.map((file, index) => (
+
onTogglePin(file)}
+ />
+ ))}
+
+ );
+}
+
+function FileRow({
+ file,
+ owner,
+ grantCount,
+ last,
+ onTogglePin,
+}: {
+ file: FileObjectRecord;
+ owner: string;
+ grantCount: number;
+ last: boolean;
+ onTogglePin: () => void;
+}) {
+ return (
+
+
+
+
+ {file.name}
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
+
+
+ {file.pinned &&
Pinned}
+
+
{formatBytes(file.sizeBytes)}
+
+
+
{file.scanState === "approved" ? "Clean" : "Blocked"}
+
{grantCount}
+
+
{formatRelative(file.updatedAt ?? file.createdAt)}
+
+
+
+
+ );
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx b/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx
new file mode 100644
index 00000000..529f6406
--- /dev/null
+++ b/AgentCompany-New/src/renderer/src/pages/files/FilesTree.tsx
@@ -0,0 +1,99 @@
+import { Icon } from "../../shared/ui";
+import type { AgentRecord, FileObjectRecord } from "../../../../shared/domain";
+import { isRecent, type Section } from "./file-helpers";
+
+export function FilesTree({
+ files,
+ pinnedFiles,
+ section,
+ departments,
+ filesByAgent,
+ onChange,
+}: {
+ files: FileObjectRecord[];
+ pinnedFiles: FileObjectRecord[];
+ section: Section;
+ departments: Array<{ name: string; count: number }>;
+ filesByAgent: Array<{ agent: AgentRecord; count: number }>;
+ onChange: (next: Section) => void;
+}) {
+ const recent = files.filter((file) => isRecent(file.createdAt));
+ return (
+
+ );
+}
+
+function TreeItem({
+ icon,
+ label,
+ count,
+ active,
+ onClick,
+}: {
+ icon: string;
+ label: string;
+ count: number;
+ active: boolean;
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts b/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts
new file mode 100644
index 00000000..44849ba0
--- /dev/null
+++ b/AgentCompany-New/src/renderer/src/pages/files/file-helpers.ts
@@ -0,0 +1,106 @@
+import type {
+ AgentRecord,
+ EmployeeRecord,
+ FileAccessGrantRecord,
+ FileObjectRecord,
+} from "../../../../shared/domain";
+
+export type ViewMode = "grid" | "list";
+
+export type Section =
+ | { kind: "all" }
+ | { kind: "recent" }
+ | { kind: "pinned" }
+ | { kind: "department"; value: string }
+ | { kind: "agent"; value: string };
+
+export const FILE_COLOR_MAP: Record = {
+ doc: { color: "#5e6ad2", label: "DOC" },
+ sheet: { color: "#2f9e58", label: "XLS" },
+ md: { color: "#52525b", label: "MD" },
+ pdf: { color: "#d23b3b", label: "PDF" },
+ img: { color: "#c87d12", label: "IMG" },
+ folder: { color: "#a1a1aa", label: "" },
+};
+
+export function collectDepartments(agents: AgentRecord[]): Array<{ name: string; count: number }> {
+ const seen = new Map();
+ for (const agent of agents) {
+ if (!agent.department) continue;
+ seen.set(agent.department, (seen.get(agent.department) ?? 0) + 1);
+ }
+ return [...seen.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => a.name.localeCompare(b.name));
+}
+
+export function collectAgentFiles(files: FileObjectRecord[], agents: AgentRecord[]): Array<{ agent: AgentRecord; count: number }> {
+ const out: Array<{ agent: AgentRecord; count: number }> = [];
+ for (const agent of agents) {
+ const count = files.filter((file) => file.ownerType === "agent" && file.ownerId === agent.id).length;
+ if (count > 0) out.push({ agent, count });
+ }
+ return out.sort((a, b) => b.count - a.count);
+}
+
+export function filterFiles(files: FileObjectRecord[], section: Section, agents: AgentRecord[]): FileObjectRecord[] {
+ if (section.kind === "all") return files;
+ if (section.kind === "pinned") return files.filter((file) => file.pinned);
+ if (section.kind === "recent") return files.filter((file) => isRecent(file.createdAt));
+ if (section.kind === "agent") return files.filter((file) => file.ownerType === "agent" && file.ownerId === section.value);
+ if (section.kind === "department") {
+ const departmentAgents = new Set(agents.filter((agent) => agent.department === section.value).map((agent) => agent.id));
+ return files.filter((file) => file.ownerType === "agent" && file.ownerId !== null && departmentAgents.has(file.ownerId));
+ }
+ return files;
+}
+
+export function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number {
+ return grants.filter((grant) => grant.fileId === fileId).length;
+}
+
+export function parseScanLabels(file: FileObjectRecord): string[] {
+ const parsed: unknown = JSON.parse(file.scanLabelsJson);
+ if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) {
+ throw new Error("File scan labels are invalid.");
+ }
+ return parsed;
+}
+
+export function shortChecksum(value: string): string {
+ if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid.");
+ return value.slice(0, 8);
+}
+
+export function shortObjectRef(value: string): string {
+ const parts = value.split(/[\\/]/).filter(Boolean);
+ if (parts.length === 0) throw new Error("File object reference is invalid.");
+ return parts.slice(-2).join("/");
+}
+
+export function isRecent(iso: string): boolean {
+ const sevenDays = 7 * 24 * 60 * 60 * 1000;
+ return Date.now() - new Date(iso).getTime() <= sevenDays;
+}
+
+export function isAgentOwner(file: FileObjectRecord): boolean {
+ return file.ownerType === "agent";
+}
+
+export function resolveOwnerName(file: FileObjectRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string {
+ if (file.ownerType === "employee" && file.ownerId) {
+ const employee = employees.find((item) => item.id === file.ownerId);
+ if (employee) return employee.displayName;
+ }
+ if (file.ownerType === "agent" && file.ownerId) {
+ const agent = agents.find((item) => item.id === file.ownerId);
+ if (agent) return agent.name;
+ }
+ return "System";
+}
+
+export function labelFor(section: Section): string {
+ if (section.kind === "all") return "All files";
+ if (section.kind === "recent") return "Recent";
+ if (section.kind === "pinned") return "Pinned";
+ if (section.kind === "agent") return "By agent";
+ return section.value;
+}