From 8bd037a98e36e28c85cbbcf5e547942b2659c986 Mon Sep 17 00:00:00 2001 From: Aviv Kaplan Date: Fri, 23 Jan 2026 19:27:53 +0200 Subject: [PATCH 1/2] fix: correct MCP server-github package name Change from non-existent @anthropic/mcp-server-github to the correct @modelcontextprotocol/server-github package. Co-Authored-By: Claude Opus 4.5 --- agentform/pr-reviewer/03-servers.af | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/agentform/pr-reviewer/03-servers.af b/agentform/pr-reviewer/03-servers.af index df7adae..cfc3754 100644 --- a/agentform/pr-reviewer/03-servers.af +++ b/agentform/pr-reviewer/03-servers.af @@ -1,8 +1,9 @@ server "github" { - command = "npx" - args = ["-y", "@anthropic/mcp-server-github"] + type = "mcp" + transport = "stdio" + command = ["npx", "-y", "@modelcontextprotocol/server-github"] - env = { - GITHUB_PERSONAL_ACCESS_TOKEN = var.github_personal_access_token + auth { + token = var.github_personal_access_token } } From f6a924d149a6918e356f453323c82fd334b24822 Mon Sep 17 00:00:00 2001 From: Aviv Kaplan Date: Tue, 27 Jan 2026 13:31:08 +0200 Subject: [PATCH 2/2] feat: add Memory panel and floating toolbar to kanban board - Add Memory types (MemoryEntry, MemoryStats, MemoryResponse) to types/index.ts - Add memory API namespace (list, stats, update, remove) to api.ts - Create useMemory hook with search, filter, edit, archive, delete - Create MemoryPanel component (Sheet with search, type tabs, entry cards, edit/delete dialogs) - Refactor kanban header from sticky bar to minimal breadcrumb line - Update QuickFilterBar with Memory toggle button and floating pill style - Wire MemoryPanel into kanban-board with cross-navigation to beads Co-Authored-By: Claude Opus 4.5 --- src/app/project/kanban-board.tsx | 59 ++- src/components/memory-panel.tsx | 613 ++++++++++++++++++++++++++++ src/components/quick-filter-bar.tsx | 28 +- src/hooks/use-memory.ts | 206 ++++++++++ src/lib/api.ts | 31 +- src/types/index.ts | 40 ++ 6 files changed, 957 insertions(+), 20 deletions(-) create mode 100644 src/components/memory-panel.tsx create mode 100644 src/hooks/use-memory.ts diff --git a/src/app/project/kanban-board.tsx b/src/app/project/kanban-board.tsx index 175c61c..831999a 100644 --- a/src/app/project/kanban-board.tsx +++ b/src/app/project/kanban-board.tsx @@ -1,9 +1,8 @@ "use client"; -import { useMemo, useRef, useState, useCallback } from "react"; +import { useMemo, useRef, useState, useCallback, useEffect } from "react"; import Link from "next/link"; import { useSearchParams, useRouter } from "next/navigation"; -import { useEffect } from "react"; import { ArrowLeft } from "lucide-react"; import { QuickFilterBar } from "@/components/quick-filter-bar"; import { Button } from "@/components/ui/button"; @@ -21,6 +20,7 @@ import { BeadDetail } from "@/components/bead-detail"; import { CommentList } from "@/components/comment-list"; import { ActivityTimeline } from "@/components/activity-timeline"; import { EditableProjectName } from "@/components/editable-project-name"; +import { MemoryPanel } from "@/components/memory-panel"; import { useBeads } from "@/hooks/use-beads"; import { useProject } from "@/hooks/use-project"; import { useBeadFilters } from "@/hooks/use-bead-filters"; @@ -96,6 +96,9 @@ export default function KanbanBoard() { // Track whether the GitHub warning has been dismissed (session-only) const [githubWarningDismissed, setGithubWarningDismissed] = useState(false); + // Memory panel state + const [isMemoryOpen, setIsMemoryOpen] = useState(false); + // Show GitHub warning if project loaded, status checked, and either no remote or not authenticated const showGitHubWarning = !projectLoading && !githubStatusLoading && @@ -234,6 +237,20 @@ export default function KanbanBoard() { setIsDetailOpen(true); }; + /** + * Handle navigation from Memory panel to a bead + * Closes the memory panel and opens the bead detail + */ + const handleMemoryNavigateToBead = useCallback((beadId: string) => { + setIsMemoryOpen(false); + // Find the bead - it may be a child task with a dot ID + const found = beads.find((b) => b.id === beadId); + if (found) { + setDetailBeadId(found.id); + setIsDetailOpen(true); + } + }, [beads]); + // Redirect state while no project ID if (!projectId) { return ( @@ -278,28 +295,23 @@ export default function KanbanBoard() { return (
- {/* Header */} -
- {/* Left: Back button - absolute positioned */} -
- -
- - {/* Center: Project name */} + {/* Breadcrumb line */} +
+ -
+
{/* Quick Filter Bar */} -
+
setIsMemoryOpen((prev) => !prev)} />
@@ -393,6 +408,16 @@ export default function KanbanBoard() { )} + {/* Memory Panel */} + {project?.path && ( + + )} + {/* GitHub Integration Warning Dialog */} !open && setGithubWarningDismissed(true)}> diff --git a/src/components/memory-panel.tsx b/src/components/memory-panel.tsx new file mode 100644 index 0000000..9a46349 --- /dev/null +++ b/src/components/memory-panel.tsx @@ -0,0 +1,613 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, + SheetFooter, +} from "@/components/ui/sheet"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogClose, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { + BrainCircuit, + Pencil, + Tag, + ExternalLink, + Archive, + Trash2, + Search, + MoreVertical, + X, + Loader2, +} from "lucide-react"; +import { useMemory } from "@/hooks/use-memory"; +import type { MemoryEntry, MemoryType } from "@/types"; + +export interface MemoryPanelProps { + /** Whether the panel is open */ + open: boolean; + /** Callback when open state changes */ + onOpenChange: (open: boolean) => void; + /** Absolute path to the project root */ + projectPath: string; + /** Callback to navigate to a bead by ID */ + onNavigateToBead?: (beadId: string) => void; +} + +type TabFilter = "all" | "learned" | "investigation"; + +/** + * Format a unix timestamp to a relative time string + */ +function formatRelativeTime(ts: number): string { + const now = Date.now() / 1000; + const diff = now - ts; + + if (diff < 60) return "just now"; + if (diff < 3600) { + const mins = Math.floor(diff / 60); + return `${mins}m ago`; + } + if (diff < 86400) { + const hours = Math.floor(diff / 3600); + return `${hours}h ago`; + } + if (diff < 604800) { + const days = Math.floor(diff / 86400); + return `${days}d ago`; + } + const date = new Date(ts * 1000); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); +} + +/** + * Format bead ID for display (shortened) + */ +function formatBeadLink(beadId: string): string { + if (!beadId) return ""; + // Show last segment: "project-id.3" -> "BD-..3" or keep short IDs as-is + const parts = beadId.split("-"); + if (parts.length > 2) { + const last = parts[parts.length - 1]; + return `BD-${last.toUpperCase()}`; + } + return beadId.toUpperCase(); +} + +/** + * Single memory entry card + */ +function MemoryEntryCard({ + entry, + onEdit, + onArchive, + onDelete, + onNavigate, +}: { + entry: MemoryEntry; + onEdit: (entry: MemoryEntry) => void; + onArchive: (key: string) => void; + onDelete: (key: string) => void; + onNavigate?: (beadId: string) => void; +}) { + return ( +
+ {/* Top row: type badge, key, timestamp */} +
+
+ {entry.type === "learned" ? ( + + LEARN + + ) : ( + + INVES + + )} + + {entry.key} + +
+ +
+ + {/* Content preview */} +

+ {entry.content} +

+ + {/* Bottom row: tags, bead link, actions menu */} +
+
+ {entry.tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} + {entry.tags.length > 4 && ( + + +{entry.tags.length - 4} + + )} +
+ +
+ {entry.bead && ( + + )} + + + + + + + onEdit(entry)} + className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100 gap-2" + > + + onEdit(entry)} + className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100 gap-2" + > + + {entry.bead && onNavigate && ( + onNavigate(entry.bead)} + className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100 gap-2" + > + + )} + + onArchive(entry.key)} + className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100 gap-2" + > + + onDelete(entry.key)} + className="text-red-400 focus:bg-zinc-800 focus:text-red-400 gap-2" + > + + + +
+
+
+ ); +} + +/** + * Memory Panel - slide-out Sheet for browsing and managing knowledge base entries + */ +export function MemoryPanel({ + open, + onOpenChange, + projectPath, + onNavigateToBead, +}: MemoryPanelProps) { + const { + stats, + isLoading, + error, + search, + setSearch, + typeFilter, + setTypeFilter, + filteredEntries, + editEntry, + archiveEntry, + deleteEntry, + } = useMemory(projectPath); + + // Tab state maps to type filter + const [activeTab, setActiveTab] = useState("all"); + + // Edit dialog state + const [editingEntry, setEditingEntry] = useState(null); + const [editContent, setEditContent] = useState(""); + const [editTags, setEditTags] = useState(""); + const [isSaving, setIsSaving] = useState(false); + + // Delete confirmation state + const [deletingKey, setDeletingKey] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + + /** + * Handle tab change - maps tab value to type filter + */ + const handleTabChange = useCallback( + (value: string) => { + const tabValue = value as TabFilter; + setActiveTab(tabValue); + setTypeFilter(tabValue === "all" ? null : (tabValue as MemoryType)); + }, + [setTypeFilter] + ); + + /** + * Handle navigate to bead + */ + const handleNavigate = useCallback( + (beadId: string) => { + onOpenChange(false); + onNavigateToBead?.(beadId); + }, + [onOpenChange, onNavigateToBead] + ); + + /** + * Open edit dialog for an entry + */ + const handleEditOpen = useCallback((entry: MemoryEntry) => { + setEditingEntry(entry); + setEditContent(entry.content); + setEditTags(entry.tags.join(", ")); + }, []); + + /** + * Save edited entry + */ + const handleEditSave = useCallback(async () => { + if (!editingEntry) return; + setIsSaving(true); + try { + const newTags = editTags + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + await editEntry(editingEntry.key, editContent, newTags); + setEditingEntry(null); + } catch { + // Error is logged in hook + } finally { + setIsSaving(false); + } + }, [editingEntry, editContent, editTags, editEntry]); + + /** + * Handle archive + */ + const handleArchive = useCallback( + async (key: string) => { + try { + await archiveEntry(key); + } catch { + // Error is logged in hook + } + }, + [archiveEntry] + ); + + /** + * Handle delete confirmation + */ + const handleDeleteConfirm = useCallback(async () => { + if (!deletingKey) return; + setIsDeleting(true); + try { + await deleteEntry(deletingKey); + setDeletingKey(null); + } catch { + // Error is logged in hook + } finally { + setIsDeleting(false); + } + }, [deletingKey, deleteEntry]); + + return ( + <> + + + + + + + {stats + ? `${stats.total} ${stats.total === 1 ? "entry" : "entries"}` + : "Loading\u2026"} + + + + {/* Search input */} +
+
+ + {/* Type filter tabs */} + + + + All + + + Learned + + + Investigation + + + + + {/* Entries list */} + +
+ {isLoading ? ( +
+
+ ) : error ? ( +
+

+ Failed to load memory entries +

+

+ {error.message} +

+
+ ) : filteredEntries.length === 0 ? ( +
+
+ ) : ( + filteredEntries.map((entry) => ( + + )) + )} +
+
+ + {/* Footer stats */} + {stats && stats.total > 0 && ( + +

+ {stats.learned} learned + + {stats.investigation}{" "} + investigation + {stats.archived > 0 && ( + <> + + {stats.archived}{" "} + archived + + )} +

+
+ )} +
+
+ + {/* Edit Dialog */} + !isOpen && setEditingEntry(null)} + > + + + + Edit Memory Entry + + + Update the content or tags for this entry. + + +
+
+ +