mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-12 00:32:38 +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)
|
// Sidebar folder ordering — listed folders appear in this order, unlisted ones follow alphabetically
|
||||||
const PINNED_FOLDERS = ['Notes']
|
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[] {
|
function sortNodes(nodes: TreeNode[]): TreeNode[] {
|
||||||
return nodes.sort((a, b) => {
|
return nodes.sort((a, b) => {
|
||||||
if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1
|
if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1
|
||||||
const aPinned = PINNED_FOLDERS.indexOf(a.name)
|
const aOrder = FOLDER_ORDER.indexOf(a.name)
|
||||||
const bPinned = PINNED_FOLDERS.indexOf(b.name)
|
const bOrder = FOLDER_ORDER.indexOf(b.name)
|
||||||
if (aPinned !== -1 && bPinned !== -1) return aPinned - bPinned
|
if (aOrder !== -1 && bOrder !== -1) return aOrder - bOrder
|
||||||
if (aPinned !== -1) return -1
|
if (aOrder !== -1) return -1
|
||||||
if (bPinned !== -1) return 1
|
if (bOrder !== -1) return 1
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}).map(node => {
|
}).map(node => {
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
|
|
@ -3151,6 +3182,31 @@ function App() {
|
||||||
return
|
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)
|
const newExpanded = new Set(expandedPaths)
|
||||||
if (newExpanded.has(path)) {
|
if (newExpanded.has(path)) {
|
||||||
newExpanded.delete(path)
|
newExpanded.delete(path)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Copy,
|
Copy,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
FilePlus,
|
FilePlus,
|
||||||
|
Folder,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
HelpCircle,
|
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
|
// Tree component for file browser
|
||||||
function Tree({
|
function Tree({
|
||||||
item,
|
item,
|
||||||
|
|
@ -999,6 +1010,7 @@ function Tree({
|
||||||
const isSelected = selectedPath === item.path
|
const isSelected = selectedPath === item.path
|
||||||
const [isRenaming, setIsRenaming] = useState(false)
|
const [isRenaming, setIsRenaming] = useState(false)
|
||||||
const isSubmittingRef = React.useRef(false)
|
const isSubmittingRef = React.useRef(false)
|
||||||
|
const displayName = (isDir && FOLDER_DISPLAY_NAMES[item.name]) || item.name
|
||||||
|
|
||||||
// For files, strip .md extension for editing
|
// For files, strip .md extension for editing
|
||||||
const baseName = !isDir && item.name.endsWith('.md')
|
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) {
|
if (!isDir) {
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
|
|
@ -1169,7 +1204,10 @@ function Tree({
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton>
|
<SidebarMenuButton>
|
||||||
<ChevronRight className="transition-transform size-4" />
|
<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>
|
</SidebarMenuButton>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<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();
|
ensureDirs();
|
||||||
ensureDefaultConfigs();
|
ensureDefaultConfigs();
|
||||||
ensureWelcomeFile();
|
|
||||||
|
|
||||||
// Initialize version history repo (async, fire-and-forget on startup)
|
// Initialize version history repo (async, fire-and-forget on startup)
|
||||||
import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {
|
import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue