diff --git a/apps/web/src/app/(authenticated)/files/page.tsx b/apps/web/src/app/(authenticated)/files/page.tsx
new file mode 100644
index 0000000..e563e1a
--- /dev/null
+++ b/apps/web/src/app/(authenticated)/files/page.tsx
@@ -0,0 +1,1436 @@
+"use client";
+
+import type { ReactElement, SyntheticEvent } from "react";
+import { useState, useEffect, useCallback, useRef } from "react";
+import { useRouter } from "next/navigation";
+import {
+ Plus,
+ Trash2,
+ List,
+ LayoutGrid,
+ Search,
+ ArrowUpDown,
+ ChevronUp,
+ ChevronDown,
+} from "lucide-react";
+import type { KnowledgeEntryWithTags } from "@mosaic/shared";
+import { EntryStatus, Visibility } from "@mosaic/shared";
+
+import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { fetchEntries, createEntry, deleteEntry } from "@/lib/api/knowledge";
+import type { EntriesResponse, CreateEntryData, EntryFilters } from "@/lib/api/knowledge";
+
+/* ---------------------------------------------------------------------------
+ Helpers
+ --------------------------------------------------------------------------- */
+
+type SortField = "title" | "updatedAt" | "createdAt";
+type SortOrder = "asc" | "desc";
+type ViewMode = "list" | "grid";
+
+interface StatusStyle {
+ label: string;
+ bg: string;
+ color: string;
+}
+
+function getStatusStyle(status: EntryStatus): StatusStyle {
+ switch (status) {
+ case EntryStatus.PUBLISHED:
+ return { label: "Published", bg: "rgba(20,184,166,0.15)", color: "var(--ms-teal-400)" };
+ case EntryStatus.DRAFT:
+ return { label: "Draft", bg: "rgba(245,158,11,0.15)", color: "var(--ms-amber-400)" };
+ case EntryStatus.ARCHIVED:
+ return { label: "Archived", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
+ default:
+ return { label: String(status), bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
+ }
+}
+
+interface VisibilityStyle {
+ label: string;
+ bg: string;
+ color: string;
+}
+
+function getVisibilityStyle(visibility: Visibility): VisibilityStyle {
+ switch (visibility) {
+ case Visibility.PUBLIC:
+ return { label: "Public", bg: "rgba(47,128,255,0.15)", color: "var(--ms-blue-400)" };
+ case Visibility.WORKSPACE:
+ return { label: "Workspace", bg: "rgba(139,92,246,0.15)", color: "var(--ms-purple-400)" };
+ case Visibility.PRIVATE:
+ return { label: "Private", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
+ default:
+ return { label: String(visibility), bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
+ }
+}
+
+function formatTimestamp(date: Date | string): string {
+ try {
+ const d = typeof date === "string" ? new Date(date) : date;
+ return d.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ } catch {
+ return String(date);
+ }
+}
+
+/* ---------------------------------------------------------------------------
+ Status / Visibility Badges
+ --------------------------------------------------------------------------- */
+
+function StatusBadge({ status }: { status: EntryStatus }): ReactElement {
+ const s = getStatusStyle(status);
+ return (
+
+ {s.label}
+
+ );
+}
+
+function VisibilityBadge({ visibility }: { visibility: Visibility }): ReactElement {
+ const v = getVisibilityStyle(visibility);
+ return (
+
+ {v.label}
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Tag Pill
+ --------------------------------------------------------------------------- */
+
+function TagPill({ name }: { name: string }): ReactElement {
+ return (
+
+ {name}
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Sort Header (for list view)
+ --------------------------------------------------------------------------- */
+
+interface SortHeaderProps {
+ label: string;
+ field: SortField;
+ currentSort: SortField;
+ currentOrder: SortOrder;
+ onSort: (field: SortField) => void;
+ style?: React.CSSProperties;
+}
+
+function SortHeader({
+ label,
+ field,
+ currentSort,
+ currentOrder,
+ onSort,
+ style,
+}: SortHeaderProps): ReactElement {
+ const isActive = currentSort === field;
+ return (
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Grid Card
+ --------------------------------------------------------------------------- */
+
+interface GridCardProps {
+ entry: KnowledgeEntryWithTags;
+ onDelete: (slug: string) => void;
+ onClick: (slug: string) => void;
+}
+
+function GridCard({ entry, onDelete, onClick }: GridCardProps): ReactElement {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
{
+ onClick(entry.slug);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onClick(entry.slug);
+ }
+ }}
+ onMouseEnter={() => {
+ setHovered(true);
+ }}
+ onMouseLeave={() => {
+ setHovered(false);
+ }}
+ style={{
+ background: "var(--surface)",
+ border: `1px solid ${hovered ? "var(--primary)" : "var(--border)"}`,
+ borderRadius: "var(--r-lg)",
+ padding: 20,
+ cursor: "pointer",
+ transition: "border-color 0.2s var(--ease)",
+ display: "flex",
+ flexDirection: "column",
+ gap: 12,
+ position: "relative",
+ }}
+ >
+ {/* Header: title + delete */}
+
+
+
+ {entry.title}
+
+
+
+
+
+ {/* Status badge */}
+
+
+
+
+
+ {/* Tags (first 2) */}
+ {entry.tags.length > 0 && (
+
+ {entry.tags.slice(0, 2).map((tag) => (
+
+ ))}
+ {entry.tags.length > 2 && (
+
+ +{String(entry.tags.length - 2)}
+
+ )}
+
+ )}
+
+ {/* Description */}
+ {entry.summary ? (
+
+ {entry.summary}
+
+ ) : null}
+
+ {/* Footer: updated date */}
+
+
+ {formatTimestamp(entry.updatedAt)}
+
+
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Submit Button (extracted to avoid inline ternary line-length issues)
+ --------------------------------------------------------------------------- */
+
+interface SubmitButtonProps {
+ isSubmitting: boolean;
+ canSubmit: boolean;
+}
+
+function SubmitButton({ isSubmitting, canSubmit }: SubmitButtonProps): ReactElement {
+ const enabled = canSubmit && !isSubmitting;
+ return (
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Create Entry Dialog
+ --------------------------------------------------------------------------- */
+
+interface CreateDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSubmit: (data: CreateEntryData) => Promise;
+ isSubmitting: boolean;
+}
+
+function CreateEntryDialog({
+ open,
+ onOpenChange,
+ onSubmit,
+ isSubmitting,
+}: CreateDialogProps): ReactElement {
+ const [title, setTitle] = useState("");
+ const [content, setContent] = useState("");
+ const [summary, setSummary] = useState("");
+ const [status, setStatus] = useState(EntryStatus.DRAFT);
+ const [visibility, setVisibility] = useState(Visibility.PRIVATE);
+ const [formError, setFormError] = useState(null);
+
+ function resetForm(): void {
+ setTitle("");
+ setContent("");
+ setSummary("");
+ setStatus(EntryStatus.DRAFT);
+ setVisibility(Visibility.PRIVATE);
+ setFormError(null);
+ }
+
+ async function handleSubmit(e: SyntheticEvent): Promise {
+ e.preventDefault();
+ setFormError(null);
+
+ const trimmedTitle = title.trim();
+ if (!trimmedTitle) {
+ setFormError("Title is required.");
+ return;
+ }
+
+ const trimmedContent = content.trim();
+ if (!trimmedContent) {
+ setFormError("Content is required.");
+ return;
+ }
+
+ try {
+ const payload: CreateEntryData = {
+ title: trimmedTitle,
+ content: trimmedContent,
+ status,
+ visibility,
+ };
+ const trimmedSummary = summary.trim();
+ if (trimmedSummary) {
+ payload.summary = trimmedSummary;
+ }
+ await onSubmit(payload);
+ resetForm();
+ } catch (err: unknown) {
+ setFormError(err instanceof Error ? err.message : "Failed to create entry.");
+ }
+ }
+
+ return (
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ Delete Confirmation Dialog
+ --------------------------------------------------------------------------- */
+
+interface DeleteDialogProps {
+ open: boolean;
+ entryTitle: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ isDeleting: boolean;
+}
+
+function DeleteConfirmDialog({
+ open,
+ entryTitle,
+ onConfirm,
+ onCancel,
+ isDeleting,
+}: DeleteDialogProps): ReactElement {
+ return (
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ File Manager Page
+ --------------------------------------------------------------------------- */
+
+export default function FileManagerPage(): ReactElement {
+ const router = useRouter();
+
+ // Data state
+ const [entries, setEntries] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // View state
+ const [viewMode, setViewMode] = useState("list");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [sortBy, setSortBy] = useState("updatedAt");
+ const [sortOrder, setSortOrder] = useState("desc");
+
+ // Debounced search
+ const searchTimerRef = useRef | null>(null);
+ const [debouncedSearch, setDebouncedSearch] = useState("");
+
+ // Create dialog state
+ const [createOpen, setCreateOpen] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+
+ // Delete dialog state
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ // Debounce search input
+ useEffect(() => {
+ if (searchTimerRef.current !== null) {
+ clearTimeout(searchTimerRef.current);
+ }
+ searchTimerRef.current = setTimeout(() => {
+ setDebouncedSearch(searchQuery);
+ }, 300);
+
+ return (): void => {
+ if (searchTimerRef.current !== null) {
+ clearTimeout(searchTimerRef.current);
+ }
+ };
+ }, [searchQuery]);
+
+ // Load entries
+ const loadEntries = useCallback(async (): Promise => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const filters: EntryFilters = {
+ sortBy,
+ sortOrder,
+ limit: 100,
+ };
+
+ if (debouncedSearch.trim()) {
+ filters.search = debouncedSearch.trim();
+ }
+
+ const response: EntriesResponse = await fetchEntries(filters);
+ setEntries(response.data);
+ } catch (err: unknown) {
+ setError(err instanceof Error ? err.message : "Failed to load entries");
+ } finally {
+ setIsLoading(false);
+ }
+ }, [sortBy, sortOrder, debouncedSearch]);
+
+ useEffect(() => {
+ void loadEntries();
+ }, [loadEntries]);
+
+ // Sort toggle handler
+ function handleSortToggle(field: SortField): void {
+ if (sortBy === field) {
+ setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
+ } else {
+ setSortBy(field);
+ setSortOrder(field === "title" ? "asc" : "desc");
+ }
+ }
+
+ // Navigation
+ function handleEntryClick(slug: string): void {
+ router.push(`/knowledge/${slug}`);
+ }
+
+ // Create
+ async function handleCreate(data: CreateEntryData): Promise {
+ setIsCreating(true);
+ try {
+ await createEntry(data);
+ setCreateOpen(false);
+ void loadEntries();
+ } finally {
+ setIsCreating(false);
+ }
+ }
+
+ // Delete
+ function handleDeleteRequest(slug: string): void {
+ const target = entries.find((e) => e.slug === slug);
+ if (target) {
+ setDeleteTarget(target);
+ }
+ }
+
+ async function handleDeleteConfirm(): Promise {
+ if (!deleteTarget) return;
+ setIsDeleting(true);
+ try {
+ await deleteEntry(deleteTarget.slug);
+ setDeleteTarget(null);
+ void loadEntries();
+ } catch (err: unknown) {
+ console.error("[FileManager] Failed to delete entry:", err);
+ setError(err instanceof Error ? err.message : "Failed to delete entry.");
+ setDeleteTarget(null);
+ } finally {
+ setIsDeleting(false);
+ }
+ }
+
+ // Retry
+ function handleRetry(): void {
+ void loadEntries();
+ }
+
+ /* ---- Render ---- */
+
+ return (
+
+ {/* Header */}
+
+
+
+ File Manager
+
+
+ Browse and manage knowledge entries
+
+
+
+
+
+
+ {/* Toolbar: Search + View toggle */}
+
+ {/* Search */}
+
+
+ {
+ setSearchQuery(e.target.value);
+ }}
+ placeholder="Search entries..."
+ style={{
+ width: "100%",
+ padding: "8px 12px 8px 36px",
+ background: "var(--surface)",
+ border: "1px solid var(--border)",
+ borderRadius: "var(--r)",
+ color: "var(--text)",
+ fontSize: "0.9rem",
+ outline: "none",
+ boxSizing: "border-box",
+ }}
+ />
+
+
+ {/* View toggle */}
+
+
+
+
+
+
+ {/* Loading state */}
+ {isLoading && entries.length === 0 ? (
+
+
+
+ ) : error !== null ? (
+ /* Error state */
+
+
{error}
+
+
+ ) : entries.length === 0 ? (
+ /* Empty state */
+
+
+ {debouncedSearch
+ ? "No entries match your search."
+ : "No files found. Create your first entry to get started."}
+
+ {!debouncedSearch && (
+
+ )}
+
+ ) : viewMode === "list" ? (
+ /* List View */
+
+ {/* Table header */}
+
+
+
+ Status
+
+
+ Visibility
+
+
+ Tags
+
+
+ {/* Actions column header - empty */}
+
+
+
+ {/* Table rows */}
+ {entries.map((entry) => (
+
+ ))}
+
+ ) : (
+ /* Grid View */
+
+ {entries.map((entry) => (
+
+ ))}
+
+ )}
+
+ {/* Create Dialog */}
+
+
+ {/* Delete Confirmation Dialog */}
+ {
+ void handleDeleteConfirm();
+ }}
+ onCancel={() => {
+ setDeleteTarget(null);
+ }}
+ isDeleting={isDeleting}
+ />
+
+ );
+}
+
+/* ---------------------------------------------------------------------------
+ List Row (extracted for hover state)
+ --------------------------------------------------------------------------- */
+
+interface ListRowProps {
+ entry: KnowledgeEntryWithTags;
+ onDelete: (slug: string) => void;
+ onClick: (slug: string) => void;
+}
+
+function ListRow({ entry, onDelete, onClick }: ListRowProps): ReactElement {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+ {
+ onClick(entry.slug);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onClick(entry.slug);
+ }
+ }}
+ onMouseEnter={() => {
+ setHovered(true);
+ }}
+ onMouseLeave={() => {
+ setHovered(false);
+ }}
+ style={{
+ display: "grid",
+ gridTemplateColumns: "1fr 110px 110px 160px 110px 40px",
+ gap: 0,
+ padding: "12px 16px",
+ borderBottom: "1px solid var(--border)",
+ cursor: "pointer",
+ background: hovered ? "var(--surface-2)" : "var(--surface)",
+ transition: "background 0.15s",
+ alignItems: "center",
+ }}
+ >
+ {/* Name */}
+
+ {entry.title}
+
+
+ {/* Status */}
+
+
+ {/* Visibility */}
+
+
+ {/* Tags */}
+
+ {entry.tags.slice(0, 2).map((tag) => (
+
+ ))}
+ {entry.tags.length > 2 && (
+
+ +{String(entry.tags.length - 2)}
+
+ )}
+
+
+ {/* Updated */}
+
+ {formatTimestamp(entry.updatedAt)}
+
+
+ {/* Delete action */}
+
+
+ );
+}