diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index b2bc9d7f..8c20dd12 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -267,18 +267,49 @@ const normalizeUsage = (usage?: Partial | null): LanguageMod } } -// Pinned folders appear first in the sidebar (in this order) -const PINNED_FOLDERS = ['Notes'] +// Sidebar folder ordering — listed folders appear in this order, unlisted ones follow alphabetically +const FOLDER_ORDER = ['People', 'Organizations', 'Projects', 'Topics', 'Meetings', 'Agent Notes', 'Notes'] -// Sort nodes (dirs first, pinned folders at top, then alphabetically) +/** + * Per-folder base view config: which columns to show and default sort. + * Folders not listed here fall back to DEFAULT_BASE_CONFIG. + */ +const FOLDER_BASE_CONFIGS: Record = { + 'Agent Notes': { + visibleColumns: ['name', 'folder', 'mtimeMs'], + sort: { field: 'mtimeMs', dir: 'desc' }, + }, + People: { + visibleColumns: ['name', 'relationship', 'organization', 'mtimeMs'], + sort: { field: 'name', dir: 'asc' }, + }, + Organizations: { + visibleColumns: ['name', 'relationship', 'mtimeMs'], + sort: { field: 'name', dir: 'asc' }, + }, + Projects: { + visibleColumns: ['name', 'status', 'topic', 'mtimeMs'], + sort: { field: 'name', dir: 'asc' }, + }, + Topics: { + visibleColumns: ['name', 'mtimeMs'], + sort: { field: 'name', dir: 'asc' }, + }, + Meetings: { + visibleColumns: ['name', 'topic', 'mtimeMs'], + sort: { field: 'mtimeMs', dir: 'desc' }, + }, +} + +// Sort nodes (dirs first, ordered folders by FOLDER_ORDER, then alphabetically) function sortNodes(nodes: TreeNode[]): TreeNode[] { return nodes.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1 - const aPinned = PINNED_FOLDERS.indexOf(a.name) - const bPinned = PINNED_FOLDERS.indexOf(b.name) - if (aPinned !== -1 && bPinned !== -1) return aPinned - bPinned - if (aPinned !== -1) return -1 - if (bPinned !== -1) return 1 + const aOrder = FOLDER_ORDER.indexOf(a.name) + const bOrder = FOLDER_ORDER.indexOf(b.name) + if (aOrder !== -1 && bOrder !== -1) return aOrder - bOrder + if (aOrder !== -1) return -1 + if (bOrder !== -1) return 1 return a.name.localeCompare(b.name) }).map(node => { if (node.children) { @@ -3151,6 +3182,31 @@ function App() { return } + // Top-level knowledge folders (except Notes) open as a bases view with folder filter + const parts = path.split('/') + if (parts.length === 2 && parts[0] === 'knowledge' && parts[1] !== 'Notes') { + const folderName = parts[1] + const folderCfg = FOLDER_BASE_CONFIGS[folderName] + setBaseConfigByPath((prev) => ({ + ...prev, + [BASES_DEFAULT_TAB_PATH]: { + ...DEFAULT_BASE_CONFIG, + name: folderName, + filters: [{ category: 'folder', value: folderName }], + ...(folderCfg && { + visibleColumns: folderCfg.visibleColumns, + sort: folderCfg.sort, + }), + }, + })) + if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) { + setIsChatSidebarOpen(false) + setIsRightPaneMaximized(false) + } + void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) + return + } + const newExpanded = new Set(expandedPaths) if (newExpanded.has(path)) { newExpanded.delete(path) diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index ee280031..6fbfa0ea 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -10,6 +10,7 @@ import { Copy, ExternalLink, FilePlus, + Folder, FolderPlus, AlertTriangle, HelpCircle, @@ -980,6 +981,16 @@ function KnowledgeSection({ ) } +function countFiles(node: TreeNode): number { + if (node.kind === 'file') return 1 + return (node.children ?? []).reduce((sum, child) => sum + countFiles(child), 0) +} + +/** Display name overrides for top-level knowledge folders */ +const FOLDER_DISPLAY_NAMES: Record = { + Notes: 'My Notes', +} + // Tree component for file browser function Tree({ item, @@ -999,6 +1010,7 @@ function Tree({ const isSelected = selectedPath === item.path const [isRenaming, setIsRenaming] = useState(false) const isSubmittingRef = React.useRef(false) + const displayName = (isDir && FOLDER_DISPLAY_NAMES[item.name]) || item.name // For files, strip .md extension for editing const baseName = !isDir && item.name.endsWith('.md') @@ -1127,6 +1139,29 @@ function Tree({ ) } + // Top-level knowledge folders (except Notes) open bases view — render as flat items + const parts = item.path.split('/') + const isBasesFolder = isDir && parts.length === 2 && parts[0] === 'knowledge' && parts[1] !== 'Notes' + + if (isBasesFolder) { + return ( + + + + onSelect(item.path, item.kind)}> + +
+ {displayName} + {countFiles(item)} +
+
+
+
+ {contextMenuContent} +
+ ) + } + if (!isDir) { return ( @@ -1169,7 +1204,10 @@ function Tree({ - {item.name} +
+ {displayName} + {countFiles(item)} +
diff --git a/apps/x/packages/core/src/config/config.ts b/apps/x/packages/core/src/config/config.ts index 453fef59..2bad01f7 100644 --- a/apps/x/packages/core/src/config/config.ts +++ b/apps/x/packages/core/src/config/config.ts @@ -29,69 +29,8 @@ function ensureDefaultConfigs() { } } -// Welcome content inlined to work with bundled builds (esbuild changes __dirname) -const WELCOME_CONTENT = `# Welcome to Rowboat - -This vault is your work memory. - -Rowboat extracts context from your emails and meetings and turns it into long-lived, editable Markdown notes. The goal is not to store everything, but to preserve the context that stays useful over time. - ---- - -## How it works - -**Entity-based notes** -Notes represent people, projects, organizations, or topics that matter to your work. - -**Auto-updating context** -As new emails and meetings come in, Rowboat adds decisions, commitments, and relevant context to the appropriate notes. - -**Living notes** -These are not static summaries. Context accumulates over time, and notes evolve as your work evolves. - ---- - -## Your AI coworker - -Rowboat uses this shared memory to help with everyday work, such as: - -- Drafting emails -- Preparing for meetings -- Summarizing the current state of a project -- Taking local actions when appropriate - -The AI works with deep context, but you stay in control. All notes are visible, editable, and yours. - ---- - -## Design principles - -**Reduce noise** -Rowboat focuses on recurring contacts and active projects instead of trying to capture everything. - -**Local and inspectable** -All data is stored locally as plain Markdown. You can read, edit, or delete any file at any time. - -**Built to improve over time** -As you keep using Rowboat, context accumulates across notes instead of being reconstructed from scratch. - ---- - -If something feels confusing or limiting, we'd love to hear about it. -Rowboat is still evolving, and your workflow matters. -`; - -function ensureWelcomeFile() { - // Create Welcome.md in knowledge directory if it doesn't exist - const welcomeDest = path.join(WorkDir, "knowledge", "Welcome.md"); - if (!fs.existsSync(welcomeDest)) { - fs.writeFileSync(welcomeDest, WELCOME_CONTENT); - } -} - ensureDirs(); ensureDefaultConfigs(); -ensureWelcomeFile(); // Initialize version history repo (async, fire-and-forget on startup) import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {