/// import { computed, equals, handler, ifElse, NAME, navigateTo, pattern, patternTool, UI, Writable, } from "commontools"; import { default as Note } from "../notes/note.tsx"; // Simple random ID generator (crypto.randomUUID not available in pattern env) const generateId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`; // Maximum number of recent pieces to track const MAX_RECENT_CHARMS = 10; import BacklinksIndex, { type MentionablePiece } from "./backlinks-index.tsx"; import SummaryIndex from "./summary-index.tsx"; import KnowledgeGraph, { getNeighborsPattern, searchGraphPattern, } from "./knowledge-graph.tsx"; import QuickCapture from "./quick-capture.tsx"; import OmniboxFAB from "./omnibox-fab.tsx"; import DoList from "../do-list/do-list.tsx"; import Notebook from "../notes/notebook.tsx"; import NotesImportExport from "../notes/notes-import-export.tsx"; import PieceGrid from "./piece-grid.tsx"; type MinimalPiece = { [NAME]?: string; isHidden?: boolean; }; type PiecesListInput = void; // Pattern returns only UI, no data outputs (only symbol properties) interface PiecesListOutput { [key: string]: unknown; backlinksIndex: { mentionable: MentionablePiece[]; }; sidebarUI: unknown; fabUI: unknown; } const _visit = handler< Record, { piece: Writable } >( (_, state) => { return navigateTo(state.piece); }, { proxy: true }, ); const removePiece = handler< Record, { piece: Writable; allPieces: Writable; } >((_, state) => { const allPiecesValue = state.allPieces.get(); const index = allPiecesValue.findIndex( (c: any) => c && state.piece.equals(c), ); if (index !== -1) { const pieceListCopy = [...allPiecesValue]; console.log("pieceListCopy before", pieceListCopy.length); pieceListCopy.splice(index, 1); console.log("pieceListCopy after", pieceListCopy.length); state.allPieces.set(pieceListCopy); } }); // Handler for dropping a note onto a notebook row const dropOntoNotebook = handler< { detail: { sourceCell: Writable } }, { notebook: Writable<{ notes?: unknown[] }> } >((event, { notebook }) => { const sourceCell = event.detail.sourceCell; const notesCell = notebook.key("notes"); const notesList = notesCell.get() ?? []; // Prevent duplicates using Writable.equals const alreadyExists = notesList.some((n) => equals(sourceCell, n as any)); if (alreadyExists) return; // Hide from Patterns list sourceCell.key("isHidden").set(true); // Add to notebook - push cell reference, not value, to maintain piece identity notesCell.push(sourceCell); }); const toggleFab = handler }>( (_, { fabExpanded }) => { fabExpanded.set(!fabExpanded.get()); }, ); // Toggle dropdown menu const toggleMenu = handler }>( (_, { menuOpen }) => menuOpen.set(!menuOpen.get()), ); // Close dropdown menu (for backdrop click) const closeMenu = handler }>( (_, { menuOpen }) => menuOpen.set(false), ); // Menu: New Note const menuNewNote = handler }>( (_, { menuOpen }) => { menuOpen.set(false); return navigateTo( Note({ title: "New Note", content: "", noteId: generateId(), }), ); }, ); // Menu: New Notebook const menuNewNotebook = handler }>( (_, { menuOpen }) => { menuOpen.set(false); return navigateTo(Notebook({ title: "New Notebook" })); }, ); // Menu: Quick Capture const menuQuickCapture = handler< void, { menuOpen: Writable; quickCapture: any } >((_, { menuOpen, quickCapture }) => { menuOpen.set(false); return navigateTo(quickCapture); }); // Helper to find existing All Notes piece const findAllNotebooksPiece = (allPieces: Writable) => { const pieces = allPieces.get(); return pieces.find((piece: any) => { const name = piece?.[NAME]; return typeof name === "string" && name.startsWith("All Notes"); }); }; // Menu: All Notes const menuAllNotebooks = handler< void, { menuOpen: Writable; allPieces: Writable } >((_, { menuOpen, allPieces }) => { menuOpen.set(false); const existing = findAllNotebooksPiece(allPieces); if (existing) { return navigateTo(existing); } return navigateTo(NotesImportExport({ importMarkdown: "", allPieces })); }); // Handler: Add piece to allPieces if not already present const addPiece = handler< { piece: MentionablePiece }, { allPieces: Writable } >(({ piece }, { allPieces }) => { const current = allPieces.get(); if (!current.some((c) => equals(c, piece))) { allPieces.push(piece); } }); // Handler: Track piece as recently used (add to front, maintain max) const trackRecent = handler< { piece: MentionablePiece }, { recentPieces: Writable } >(({ piece }, { recentPieces }) => { const current = recentPieces.get(); // Remove if already present const filtered = current.filter((c) => !equals(c, piece)); // Add to front and limit to max const updated = [piece, ...filtered].slice(0, MAX_RECENT_CHARMS); recentPieces.set(updated); }); /** Read current do list items */ const readDoList = pattern< { items: Array<{ title: string; done: boolean; indent: number }> }, { result: Array<{ title: string; done: boolean; indent: number }> } >(({ items }) => { return { result: items }; }); const benExtraSystemPrompt = ` Do-list management: - When users mention tasks, action items, or things to do, use addDoItem or addDoItems - When users paste a block of text with multiple items, parse into items and use addDoItems to batch-add - Use readDoList to check current items before making changes - Use updateDoItem to mark done or rename; removeDoItem only for explicit deletion - Use indent levels for sub-tasks (0=root, 1=sub-task, 2=sub-sub-task) Knowledge graph: - For finding relationships between pieces: use getNeighbors with an entity reference to get all incoming/outgoing links, or searchAnnotations to search agent-created annotations by text `; export default pattern((_) => { // OWN the data cells (not from wish) const allPieces = Writable.of([]); const recentPieces = Writable.of([]); // Dropdown menu state const menuOpen = Writable.of(false); // Filter out hidden pieces and pieces without resolved NAME // (prevents transient hash-only pills during reactive updates) // NOTE: Use truthy check, not === true, because piece.isHidden is a proxy object const visiblePieces = computed(() => allPieces.get().filter((piece) => { if (!piece) return false; if (piece.isHidden) return false; const name = piece?.[NAME]; return typeof name === "string" && name.length > 0; }) ); const doListItems = Writable.of([]); const doList = DoList({ items: doListItems }); // Combine user-managed allPieces with system pieces (like doList) so // BacklinksIndex picks up their mentionable items. const allPiecesWithSystem = computed(() => [ ...allPieces.get(), doList as any, ]); const index = BacklinksIndex({ allPieces: allPiecesWithSystem }); const summaryIdx = SummaryIndex({}); const knowledgeGraph = KnowledgeGraph({}); const quickCapture = QuickCapture({ allPieces }); const fab = OmniboxFAB({ mentionable: index.mentionable, extraTools: { addDoItem: { handler: doList.addItem, description: "Add a task to the do list. Use indent for sub-tasks (0=root, 1=sub, 2=sub-sub). Pass attachments array to link pieces.", }, addDoItems: { handler: doList.addItems, description: "Add multiple tasks at once. Each item can have attachments to link pieces.", }, removeDoItem: { handler: doList.removeItemByTitle, description: "Remove a task and its subtasks by title.", }, updateDoItem: { handler: doList.updateItemByTitle, description: "Update a task by title. Set done=true to complete, newTitle to rename, attachments to link pieces.", }, readDoList: patternTool(readDoList, { items: doList.items, }), getNeighbors: patternTool(getNeighborsPattern, { edges: knowledgeGraph.edges, }), searchAnnotations: patternTool(searchGraphPattern, { edges: knowledgeGraph.edges, compoundNodes: knowledgeGraph.compoundNodes, }), }, extraSystemPrompt: benExtraSystemPrompt, }); const gridView = PieceGrid({ pieces: visiblePieces }); const recentGridView = PieceGrid({ pieces: recentPieces }); return { backlinksIndex: index, summaryIndex: summaryIdx, knowledgeGraph, quickCapture, [NAME]: computed(() => `Ben's Space (${visiblePieces.length})`), [UI]: (

Patterns

Mentions Search Graph
Notes ▾ {/* Backdrop to close menu when clicking outside */}
(menuOpen.get() ? "block" : "none")), position: "fixed", inset: "0", zIndex: "999", }} /> {/* Dropdown Menu */} (menuOpen.get() ? "flex" : "none")), position: "fixed", top: "112px", right: "16px", background: "var(--ct-color-bg, white)", border: "1px solid var(--ct-color-border, #e5e5e7)", borderRadius: "12px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", minWidth: "160px", zIndex: "1000", padding: "4px", }} > {"\u00A0\u00A0"}📝 New Note {"\u00A0\u00A0"}📓 New Notebook {"\u00A0\u00A0"}⚡ Quick Capture
{"\u00A0\u00A0"}📁 All Notes

Do List

{doList.compactUI}
{ifElse( computed(() => recentPieces.get().length > 0),

Recent

{recentPieces.map((piece: any) => ( ))}
, undefined, )}

Pieces

{visiblePieces.map((piece) => { const isNotebook = computed(() => { const name = piece?.[NAME]; const result = typeof name === "string" && name.startsWith("📓"); return result; }); const link = ( ); return ( {ifElse( isNotebook, {link} , link, )} 🗑️ ); })}
), sidebarUI: undefined, fabUI: fab[UI], // Exported data allPieces, recentPieces, // Exported handlers (bound to state cells for external callers) addPiece: addPiece({ allPieces }), trackRecent: trackRecent({ recentPieces }), pinToChat: fab.pinToChat, }; });