Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5100119
feat(desktop): link Markdown files and folders alongside Vault copies
Kechos23 Sep 29, 2026
7859e8a
docs(test): explain linked sources and adjust menu assertions
Kechos23 Sep 29, 2026
1f7aaa3
Merge remote-tracking branch 'origin/main' into codex/linked-markdown…
Kechos23 Oct 3, 2026
0bdf529
fix(desktop): distinguish linked files and folders with symlink icons
Kechos23 Oct 3, 2026
00ebcc6
fix(sidebar): group linked documents under Workspaces and persist fol…
Kechos23 Oct 3, 2026
5bd34a9
fix(desktop): preserve Welcome document on initial file launch
Kechos23 Oct 3, 2026
b74bec0
fix(storage): isolate linked recovery drafts from the normal Vault
Kechos23 Oct 3, 2026
c7f7203
fix(desktop): keep converted Vault copies independent from linked ori…
Kechos23 Oct 3, 2026
ce6a5ab
fix(desktop): clarify linked removal actions and simplify context menus
Kechos23 Oct 3, 2026
7fbee44
fix(ui): align desktop toast status icons with existing notifications
Kechos23 Oct 3, 2026
b8b2ee3
fix(desktop): restore excluded links on explicit folder rescans
Kechos23 Oct 3, 2026
4756b86
fix(sidebar): show linked source paths only on hover
Kechos23 Oct 3, 2026
d1c5d85
test(desktop): cover linked workspace review fixes and storage migration
Kechos23 Oct 3, 2026
82aeb5f
feat(desktop): convert linked sidebar drops into destination Vault co…
Kechos23 Oct 3, 2026
8262567
feat(desktop): preserve source documents when saving new linked copies
Kechos23 Oct 3, 2026
ee2dab8
fix(web): guard linked workspace and native save behind desktop capab…
Kechos23 Oct 3, 2026
c346baa
fix(desktop): preserve context menus during linked background refresh
Kechos23 Oct 3, 2026
8c3885a
feat(desktop): add default Linked Workspace guide document
Kechos23 Oct 3, 2026
7910a12
test(desktop): validate linked workflow without custom runtime hooks
Kechos23 Oct 3, 2026
2f23673
fix(web): retain the existing file-import label outside desktop
Kechos23 Oct 3, 2026
c09fdc7
test(web): preserve the browser Workspace move destination label
Kechos23 Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions assets/linked-workspace-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Welcome to Linked Workspace

Linked Workspace lets you edit Markdown files where they already live on your computer.

## Link a file or folder

Use **Link Markdown files** or **Link a Markdown folder** in the Explorer toolbar or the New menu. A linked folder includes Markdown files in its subfolders.

Linked documents use a file-with-arrow icon; linked folders use a folder-with-arrow icon. Hover over a linked document to see its original path.

## Drafts and saving

Editing automatically stores a recovery draft inside the application's **Linked Workspace** storage folder. It does **not** automatically overwrite your original file.

- **Ctrl+S** or **Save** updates the original linked file.
- **Save As** creates a new external file and a separate link. The previous document and original remain unchanged.
- Saving a Vault document externally keeps its independent Vault copy and activates the new linked document.

## Independent Vault copies

**Convert to Workspace copy** turns a linked draft into an independent Vault document without changing the original. You can also drag a linked document into the Vault or one of its folders.

Reopening the original creates or activates its link; it never replaces the independent Vault copy.

## External changes and rescanning

The application checks linked sources for external changes. If your local draft conflicts with a changed original, it keeps the draft and shows a warning.

**Rescan folder** discovers new files and restores links previously removed or converted, if the originals still exist. Re-linking the same folder does the same. Automatic background refreshes respect removed links instead of restoring them.

## Closing and removing

Closing a tab keeps its document in the workspace. **Remove linked file** or **Remove linked folder** removes links from the application, not the originals from disk.

This guide is an internal, editable document, not a link to an external file. You can delete it without deleting any source file; it will not reappear on every launch.
3 changes: 3 additions & 0 deletions assets/lucide-icons.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Desktop-only files:
- Document metadata loads at startup; Markdown content loads only when a document is opened and is kept in a bounded in-memory cache.
- Native Markdown/HTML save and Markdown open flows use Neutralino dialogs and filesystem APIs.
- A Markdown file passed as a launch argument is loaded into the editor.
- Files opened from disk can remain linked to their original path. Workspace (Vault) copies are separate documents; converting a linked file to a copy leaves its source untouched.
- Linked files keep an app-local draft, but write to the original only on Ctrl+S. Reopening the same path selects its existing document rather than making another copy.
- **Link a Markdown folder** adds its Markdown files under Linked Workspace, alongside Workspace (Vault) and Secret Workspace. Nested folders can be expanded independently; source paths appear on hover. Files added or changed outside the app are detected while the desktop app is running; a locally edited draft is not silently overwritten by an external change.
- Linked recovery drafts are stored separately under `Markdown Viewer Vault/Linked Workspace`; normal copies remain under `Markdown Viewer Vault/Workspace`. Existing linked drafts migrate safely. The default Linked Workspace guide is editable and is not recreated after intentional deletion.
- Converted Vault copies stay independent even when the original is reopened. Explicit **Rescan folder** or re-adding the folder restores removed links without replacing those copies; background scans respect removals.
- Linked context menus offer **Save** and **Save As**. Save updates the current source; Save As creates a separate external file and link. Vault Save As (or Ctrl+S) preserves the Vault document and focuses the external linked copy. Dialogs propose the document title and add `.md` when no extension is given. Ctrl+S also works with non-Latin keyboard layouts; Ctrl+Shift+S retains scroll-sync behavior.
- Dragging a linked document onto the Vault root or a Vault folder converts it into an independent copy there. The original is unchanged. Secret Workspace and reverse Vault-to-linked drops are not supported. Ordinary Vault moves use the existing HTML drag-and-drop behavior.
- Native linked controls and Save/Save As actions are desktop-only. Browser/Docker users retain normal stored documents and Markdown downloads. Desktop workflows are validated on Windows; macOS/Linux have not been manually verified. No custom Neutralino runtime is required by this feature.
- Removing a link removes the app's reference, not the original file or folder. Native file drops are not covered by the linked-file flow in this build.
- The app asks before closing the window.
- Prepared desktop resources load dynamic libraries from local `/libs/...` paths after setup.
- Private mode pauses document-state persistence for the current page session without deleting the vault, shows an active incognito status, and resets on reload or exit. **Reset workspace** permanently deletes documents, folders, review data, settings, Secret Workspace records, history, trash, and recovery journals after confirmation.
Expand Down Expand Up @@ -81,7 +90,7 @@ Seven self-contained executables are written to `desktop-app/dist/markdown-viewe
| Token security | One-time |
| Logging | Disabled |

Native APIs are intentionally allowlisted: app exit, open/save dialogs, message boxes, external URL/folder open, tray setup, scoped file and folder operations, path lookup, and storage get/set/remove. Command execution is not part of the default allowlist.
Native APIs are intentionally allowlisted: app exit, open/save/folder dialogs, message boxes, external URL/folder open, tray setup, scoped file and folder operations, filesystem watchers, path lookup, and storage get/set/remove. Command execution is not part of the default allowlist.

## Local Renderer Security

Expand Down
4 changes: 4 additions & 0 deletions desktop-app/neutralino.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"nativeAllowList": [
"app.exit",
"os.showOpenDialog",
"os.showFolderDialog",
"os.showSaveDialog",
"os.showMessageBox",
"os.getPath",
Expand All @@ -34,6 +35,9 @@
"filesystem.copy",
"filesystem.move",
"filesystem.remove",
"filesystem.createWatcher",
"filesystem.removeWatcher",
"filesystem.getWatchers",
"storage.setData",
"storage.getData",
"storage.getKeys",
Expand Down
10 changes: 9 additions & 1 deletion desktop-app/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ <h1 class="h4 mb-0">Markdown Viewer<span class="visually-hidden"> - Online Markd
</button>
<ul class="dropdown-menu app-command-menu header-command-menu header-new-menu" aria-labelledby="importDropdown">
<li><a class="dropdown-item app-menu-item" href="#" id="header-new-document"><i class="lucide lucide-file-plus-2" aria-hidden="true"></i><span class="app-menu-label">New document</span></a></li>
<li><a class="dropdown-item app-menu-item" href="#" id="import-from-file"><i class="lucide lucide-upload" aria-hidden="true"></i><span class="app-menu-label">From files</span></a></li>
<li><a class="dropdown-item app-menu-item" href="#" id="import-from-file"><i class="lucide lucide-files" aria-hidden="true"></i><span class="app-menu-label">From files</span></a></li>
<li class="desktop-linked-command" hidden><a class="dropdown-item app-menu-item" href="#" id="link-from-file"><i class="lucide lucide-file-symlink" aria-hidden="true"></i><span class="app-menu-label">Link files…</span></a></li>
<li class="desktop-linked-command" hidden><a class="dropdown-item app-menu-item" href="#" id="link-from-folder"><i class="lucide lucide-folder-symlink" aria-hidden="true"></i><span class="app-menu-label">Link folder…</span></a></li>
<li><a class="dropdown-item app-menu-item" href="#" id="import-from-github"><i class="bi bi-github" aria-hidden="true"></i><span class="app-menu-label">From GitHub</span></a></li>
</ul>
</div>
Expand Down Expand Up @@ -346,6 +348,12 @@ <h2 id="document-sidebar-title">Explorer</h2>
<button id="sidebar-new-folder" class="document-sidebar-icon-btn" type="button" aria-label="Create a folder" title="New folder">
<i class="lucide lucide-folder-plus" aria-hidden="true"></i>
</button>
<button id="sidebar-link-files" class="document-sidebar-icon-btn desktop-linked-command" type="button" aria-label="Link Markdown files" title="Link Markdown files" hidden>
<i class="lucide lucide-file-symlink" aria-hidden="true"></i>
</button>
<button id="sidebar-link-folder" class="document-sidebar-icon-btn desktop-linked-command" type="button" aria-label="Link a Markdown folder" title="Link a Markdown folder" hidden>
<i class="lucide lucide-folder-symlink" aria-hidden="true"></i>
</button>
<button id="sidebar-trash-button" class="document-sidebar-icon-btn" type="button" aria-label="Open Trash" title="Trash">
<i class="lucide lucide-trash-2" aria-hidden="true"></i>
</button>
Expand Down
13 changes: 11 additions & 2 deletions desktop-app/resources/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ if (isNeutralinoRuntime()) {
// TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
setTray();
}

window.addEventListener('markdown-viewer:ready', function() {
if (typeof window.NL_START_LINKED_MONITORING === 'function') {
window.NL_START_LINKED_MONITORING().catch(function(error) {
console.warn('Linked source monitoring initialization failed:', error);
});
}
}, { once: true });
}

// Open file passed as command-line argument (e.g. when double-clicking a .md file)
Expand All @@ -146,12 +154,13 @@ if (isNeutralinoRuntime()) {

window.NL_INITIAL_FILE_CONTENT = {
name: fileName,
content: content
content: content,
sourcePath: filePath
};

// Callback hook in case script.js loaded first
if (window.NL_IMPORT_EXTERNAL_FILE) {
window.NL_IMPORT_EXTERNAL_FILE(content, fileName);
window.NL_IMPORT_EXTERNAL_FILE(content, fileName, filePath);
}
} catch (e) {
console.warn('Could not open initial file:', e);
Expand Down
Loading
Loading