mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
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:
parent
903fecc5f5
commit
3f81b771b2
3 changed files with 103 additions and 70 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue