Notes org (#458)

* show count in notes

* bases view for everythign other than notes

* added folder column config

* removed welcome note and reordered
This commit is contained in:
arkml 2026-03-31 17:13:52 +05:30 committed by GitHub
parent 903fecc5f5
commit 3f81b771b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 70 deletions

View file

@ -267,18 +267,49 @@ const normalizeUsage = (usage?: Partial<LanguageModelUsage> | 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<string, { visibleColumns: string[]; sort: { field: string; dir: 'asc' | 'desc' } }> = {
'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)

View file

@ -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<string, string> = {
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 (
<ContextMenu>
<ContextMenuTrigger asChild>
<SidebarMenuItem>
<SidebarMenuButton onClick={() => onSelect(item.path, item.kind)}>
<Folder className="size-4 shrink-0" />
<div className="flex w-full items-center gap-1 min-w-0">
<span className="min-w-0 flex-1 truncate">{displayName}</span>
<span className="text-xs text-sidebar-foreground/50 tabular-nums shrink-0">{countFiles(item)}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</ContextMenuTrigger>
{contextMenuContent}
</ContextMenu>
)
}
if (!isDir) {
return (
<ContextMenu>
@ -1169,7 +1204,10 @@ function Tree({
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<ChevronRight className="transition-transform size-4" />
<span>{item.name}</span>
<div className="flex w-full items-center gap-1 min-w-0">
<span className="min-w-0 flex-1 truncate">{displayName}</span>
<span className="text-xs text-sidebar-foreground/50 tabular-nums shrink-0">{countFiles(item)}</span>
</div>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>

View file

@ -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 => {