fix: restore vault images and default folders

This commit is contained in:
lucaronin
2026-04-23 22:36:10 +02:00
parent 941f12aa1b
commit accc6270d3
7 changed files with 32 additions and 27 deletions

View File

@@ -305,7 +305,7 @@ type SidebarSelection =
- Parses dates as ISO 8601 to Unix timestamps
- Extracts relationships, outgoing links, custom properties, word count, snippet
The folder tree is narrower than the scanner: it exposes user-created folders only, while system folders such as `attachments/`, `type/`, and `views/` remain hidden from folder navigation.
The folder tree hides only the dedicated `type/` directory, since note types already have their own sidebar section. Default vault folders such as `attachments/` and `views/` remain visible alongside user-created folders.
6. Sorts by `modified_at` descending
7. Skips unparseable files with a warning log

View File

@@ -176,7 +176,7 @@ flowchart TD
└──────────────────────────────────────────────────────────────┘
```
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), collapsible type-based section groups, and a dedicated folder tree. The folder tree shows user-created folders only; system folders such as `attachments/`, `type/`, and `views/` remain scannable but are hidden from folder navigation. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), collapsible type-based section groups, and a dedicated folder tree. The folder tree shows user-created folders plus default vault folders such as `attachments/` and `views/`; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
- **Note List / Pulse View** (200-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count, BlockNote rich text editor with wikilink support, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files) or raw CodeMirror view. Decomposed into `Editor` (orchestrator), `EditorContent`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for markdown source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Navigation history (Cmd+[/]) replaces tabs.
- **Inspector / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history) and AI Agent panel (the selected CLI agent with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. Per-note `icon` is a suggested Inspector property and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).

View File

@@ -213,8 +213,8 @@ pub fn reload_entry(path: &Path) -> Result<VaultEntry, String> {
/// Directories hidden from user-facing vault scans.
const HIDDEN_DIRS: &[&str] = &[".git", ".laputa", ".DS_Store"];
/// System folders that hold Tolaria metadata or assets rather than user note groups.
const FOLDER_TREE_EXCLUDED_DIRS: &[&str] = &["attachments", "type", "views"];
/// Keep type definitions in their dedicated sidebar section instead of the generic folder tree.
const FOLDER_TREE_EXCLUDED_DIRS: &[&str] = &["type"];
fn is_hidden_dir(name: &str) -> bool {
name.starts_with('.') || HIDDEN_DIRS.contains(&name)

View File

@@ -34,7 +34,7 @@ fn test_scan_vault_folders_excludes_hidden() {
}
#[test]
fn test_scan_vault_folders_excludes_system_asset_folders() {
fn test_scan_vault_folders_keeps_default_vault_folders_visible() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("attachments")).unwrap();
std::fs::create_dir_all(dir.path().join("type")).unwrap();
@@ -44,7 +44,7 @@ fn test_scan_vault_folders_excludes_system_asset_folders() {
let folders = scan_vault_folders(dir.path()).unwrap();
let names: Vec<&str> = folders.iter().map(|folder| folder.name.as_str()).collect();
assert_eq!(names, vec!["projects"]);
assert_eq!(names, vec!["attachments", "projects", "views"]);
}
#[test]

View File

@@ -1,6 +1,7 @@
import { act, render, screen, fireEvent, waitFor, within } from '@testing-library/react'
import type { ReactNode } from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { DEFAULT_VAULTS } from './hooks/useVaultSwitcher'
// Provide a localStorage mock that supports all methods (jsdom's may be incomplete)
const localStorageMock = (() => {
@@ -95,6 +96,9 @@ const mockVaultList = {
hidden_defaults: [],
}
const mockDefaultVaultPath = '/Users/mock/Documents/Getting Started'
const expectedDefaultVaultPath = DEFAULT_VAULTS[0].path || mockDefaultVaultPath
const mockCommandResults: Record<string, unknown> = {
load_vault_list: mockVaultList,
list_vault: mockEntries,
@@ -108,7 +112,7 @@ const mockCommandResults: Record<string, unknown> = {
git_pull: { status: 'up_to_date', message: 'Already up to date', updatedFiles: [], conflictFiles: [] },
save_settings: null,
check_vault_exists: true,
get_default_vault_path: '/Users/mock/Documents/Getting Started',
get_default_vault_path: expectedDefaultVaultPath,
list_themes: [],
get_vault_settings: { theme: null },
}
@@ -245,7 +249,7 @@ function resetMockCommandResults() {
},
save_settings: null,
check_vault_exists: true,
get_default_vault_path: '/Users/mock/Documents/Getting Started',
get_default_vault_path: expectedDefaultVaultPath,
list_themes: [],
get_vault_settings: { theme: null },
})
@@ -421,7 +425,7 @@ describe('App', () => {
release_channel: null,
}
mockCommandResults.load_vault_list = { vaults: [], active_vault: null, hidden_defaults: [] }
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === '/Users/mock/Documents/Getting Started'
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
render(<App />)
@@ -441,7 +445,7 @@ describe('App', () => {
['telemetry-accept', 'Allow anonymous reporting'],
['telemetry-decline', 'No thanks'],
])('ignores a remembered default vault after %s when onboarding was never completed', async (buttonTestId) => {
const rememberedDefaultVaultPath = '/Volumes/Jupiter/Workspace/laputa-app/demo-vault-v2'
const rememberedDefaultVaultPath = expectedDefaultVaultPath
localStorage.setItem('tolaria_welcome_dismissed', '1')
mockCommandResults.get_default_vault_path = rememberedDefaultVaultPath
mockCommandResults.get_settings = {
@@ -517,7 +521,7 @@ describe('App', () => {
active_vault: '/missing-vault',
hidden_defaults: [],
}
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === '/Users/mock/Documents/Getting Started'
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
render(<App />)
@@ -534,7 +538,7 @@ describe('App', () => {
active_vault: null,
hidden_defaults: [],
}
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === '/Users/mock/Documents/Getting Started'
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
render(<App />)

View File

@@ -12,11 +12,11 @@ vi.mock('../mock-tauri', () => ({
}))
function assetUrl(path: string): string {
return `http://asset.localhost/${encodeURIComponent(path)}`
return `asset://localhost/${encodeURIComponent(path)}`
}
function legacyAssetUrl(path: string): string {
return `asset://localhost/${encodeURIComponent(path)}`
function httpAssetUrl(path: string): string {
return `http://asset.localhost/${encodeURIComponent(path)}`
}
describe('resolveImageUrls', () => {
@@ -53,7 +53,7 @@ describe('resolveImageUrls', () => {
it('rewrites legacy asset URLs from a different vault', () => {
tauriMode = true
const legacyUrl = legacyAssetUrl('/Users/luca/Workspace/tolaria-getting-started/attachments/CleanShot.png')
const legacyUrl = assetUrl('/Users/luca/Workspace/tolaria-getting-started/attachments/CleanShot.png')
const markdown = `![CleanShot](${legacyUrl})`
expect(resolveImageUrls(markdown, '/Users/john/Documents/Getting Started')).toBe(
@@ -61,6 +61,14 @@ describe('resolveImageUrls', () => {
)
})
it('leaves already-correct http asset URLs unchanged', () => {
tauriMode = true
const url = httpAssetUrl('/vault/attachments/file.png')
const markdown = `![alt](${url})`
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
})
it('leaves external URLs unchanged', () => {
tauriMode = true
const httpImage = '![logo](https://example.com/logo.png)'
@@ -72,7 +80,7 @@ describe('resolveImageUrls', () => {
it('handles multiple images in one document', () => {
tauriMode = true
const markdown = `![a](${legacyAssetUrl('/old/attachments/a.png')})\n\n![b](attachments/b.png)`
const markdown = `![a](${assetUrl('/old/attachments/a.png')})\n\n![b](attachments/b.png)`
const result = resolveImageUrls(markdown, '/vault')
@@ -91,7 +99,7 @@ describe('resolveImageUrls', () => {
it('skips unknown asset URLs without an attachments segment', () => {
tauriMode = true
const url = assetUrl('/some/other/path/file.png')
const url = httpAssetUrl('/some/other/path/file.png')
const markdown = `![alt](${url})`
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
@@ -109,7 +117,7 @@ describe('portableImageUrls', () => {
})
it('converts legacy asset protocol attachment URLs to relative paths', () => {
const url = legacyAssetUrl('/vault/attachments/legacy.png')
const url = httpAssetUrl('/vault/attachments/legacy.png')
const markdown = `![screenshot](${url})`
expect(portableImageUrls(markdown, '/vault')).toBe(

View File

@@ -17,7 +17,7 @@ type MarkdownImageUrl = string
const MD_IMAGE_PATTERN = /!\[([^\]]*)\]\(([^)\s"]+)(\s+"[^"]*")?\)/g
function assetUrl(path: AbsolutePath): MarkdownImageUrl {
return normalizeWebviewAssetUrl(convertFileSrc(path), path)
return convertFileSrc(path)
}
function vaultAttachmentPath(vaultPath: VaultPath, attachmentPath: AttachmentPath): AbsolutePath {
@@ -32,13 +32,6 @@ function extractAttachmentPath(absolutePath: AbsolutePath): AttachmentPath | nul
return filename ? `${RELATIVE_ATTACHMENTS_PREFIX}${filename}` : null
}
function normalizeWebviewAssetUrl(url: MarkdownImageUrl, path: AbsolutePath): MarkdownImageUrl {
if (url.startsWith(ASSET_URL_PREFIX)) {
return `${HTTP_ASSET_URL_PREFIX}${encodeURIComponent(path)}`
}
return url
}
function assetUrlPrefix(url: MarkdownImageUrl): string | null {
return ASSET_URL_PREFIXES.find(prefix => url.startsWith(prefix)) ?? null
}