Pins API
The Pins API manages content-directory pins addressed by array index (pinIndex) — distinct from headerLeftPins (the toolbar layout-template pins, see Pins). The commands below (pins_list, pins_read, …) are unchanged across backends, but path resolution differs by mode (epic #4204, cloud-primary pivot):
Cloud workspace mode (the default, desktop and web) — every pin is addressed at the fixed workspace-relative path
pins/<pinIndex>(D4'spinsnamespace).AppSettings.pins(thepath/titleconfig below) is not consulted — there is no way to point a pin at an arbitrary directory, since cloud-primary has no concept of "arbitrary local directory" outside the workspace (only External File Editor addresses local disk, D5).Local engine (
tauri-,app/ core/ src/ pins. rs http_server.rs's/routes) — used only byapi/ pins/ * pnpm dev:rest's non-workspace fallback. This is the pre-pivot implementation described below: it readsAppSettings.pinsfrom disk and resolvespathagainst the local project root. It is slated for removal in S19 (#4223) once REST's non-workspace fallback goes away.
Note
If you are integrating against the cloud-workspace behavior, skip straight to the Commands section — the request/response shapes are identical; only the "Pin Configuration" section below is local-engine-only.
Data Structures
PinEntry
Returned by pins_list. Represents a file or directory in the pin tree.
interface PinEntry {
name: string; // Filename or directory name
type: 'file' | 'directory';
path: string; // Relative path from pin root (e.g. "subdir/file.md")
title: string; // From frontmatter `name` or `title`, or filename stem
description: string; // From frontmatter `description`, or empty string
modifiedAt: string; // ISO 8601 timestamp (files only, empty for directories)
children?: PinEntry[]; // Populated only in deep (list_pin_tree) mode; absent in lazy mode
hasChildren?: boolean; // Set for directories in lazy mode — indicates whether a directory
// has at least one visible child; absent for file entries
}The title field prefers the frontmatter name key over title. Directories use their directory name as the title.
Pin Configuration (local engine only)
This section describes the tauri- local engine's reading of .zudotext.settings.json's pins array — the pnpm dev:rest non-workspace fallback only (see the note at the top of this page). Cloud workspace mode ignores this array entirely and always addresses pins/<pinIndex> inside the workspace.
AppSettings.pins is a typed PinConfigEntry[] (@takazudo/app-defaults, epic #4204 D4):
{
"pins": [
{ "path": ".zudotext/skills", "title": "Skills" },
{ "path": "notes", "title": "Notes", "type": "directory" }
]
}If no pins are configured, the default pin { "path": "pins", "title": "Pins", "type": "directory" } is used (defined in @takazudo/app-defaults).
Path resolution (local engine):
pathmust be workspace-relative POSIX — no leading/, no..segment (validateSettingsdrops any entry that fails this check, falling back to the default pin if none remain). This was relaxed pre-pivot to also accept local absolute paths (/, for pointing a pin at an arbitrary directory outside the project root via the native file picker); that capability has no equivalent under cloud-primary (D4/D5 — External File Editor is the only local-file provider) and is no longer accepted for workspace pins.absolute/ path/ to/ docs The local engine resolves
pathrelative to the project root, with traversal checking (safe_path) — paths that escape the project root are rejected at read/write time regardless of whatvalidateSettingsalready filtered.
Commands
pins_list
List files and directories in a pin as a tree structure.
const entries = await invoke<PinEntry[]>('pins_list', {
pinIndex: 0,
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
Returns: Result<PinEntry[], string> — the top-level entries only (one level deep).
Behavior:
Lists only the immediate children of the pin root — does not recurse
For directory entries, sets
hasChildren: true/falseto indicate whether the directory has visible children; does not populatechildrenOnly includes
.mdfiles and directories; skips dotfiles and non-markdown filesSorts: directories first, then alphabetically by name
For file-type pins, returns a single-item list for the file itself
Use
pins_list_childrento lazily expand individual directories
pins_read
Read the content of a file within a pin directory.
const content = await invoke<string | null>('pins_read', {
pinIndex: 0,
entryPath: 'subdir/file.md',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
entryPath | string | Relative path within the pin directory |
Returns: Result<string | null, string> — file content, or null if the file doesn't exist.
Security: The entryPath is validated through safe_path() to prevent directory traversal.
pins_write
Write content to a file within a pin directory.
const success = await invoke<boolean>('pins_write', {
pinIndex: 0,
entryPath: 'file.md',
content: '---\ntitle: Updated\n---\nNew content',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
entryPath | string | Relative path within the pin directory |
content | string | Full file content |
Returns: Result<boolean, string> — true on success.
Behavior: Returns an error if the parent directory doesn't exist. Does not create intermediate directories.
pins_delete
Delete a file within a pin directory.
const success = await invoke<boolean>('pins_delete', {
pinIndex: 0,
entryPath: 'old-file.md',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
entryPath | string | Relative path within the pin directory |
Returns: Result<boolean, string> — true on success (including if the file was already missing).
pins_list_children
Fetch the immediate children of a directory within a pin (lazy expansion).
const entries = await invoke<PinEntry[]>('pins_list_children', {
pinIndex: 0,
dirPath: 'subdir',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
dirPath | string | Relative path from the pin root to the directory to expand |
Returns: Result<PinEntry[], string> — one level of entries with hasChildren flags, same shape as pins_list.
Behavior: Called by the frontend when the user expands a directory node. The dirPath is validated against safe_path() so it cannot escape the pin root. Returns an error for file-type pins.
pins_create_file
Create a new empty file in a directory-type pin, using a timestamp-based filename.
const filename = await invoke<string>('pins_create_file', {
pinIndex: 0,
});
// filename: "20250308143045-new.md"| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
Returns: Result<string, string> — the generated filename on success.
Behavior: Creates the pin directory if it doesn't exist. Returns an error for file-type pins.
pins_write_new_file
Write a new file into a directory-type pin. Fails if the file already exists ("write-once / no-overwrite" semantics).
const filename = await invoke<string>('pins_write_new_file', {
pinIndex: 0,
filename: 'my-note.md',
content: '---\ntitle: My Note\n---\nContent',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index into the pins settings array |
filename | string | The filename to create |
content | string | Full file content |
Returns: Result<string, string> — the filename on success.
Typed errors (stable string prefixes):
| Error prefix | Meaning |
|---|---|
"collision" | File already exists |
"file-pin-not-supported" | Pin is a file-type pin |
"InvalidFilename: ..." | Empty, contains path separators, or .. in name |
"io: ..." | Disk or permission error |
Watcher Commands
Pin files live in the workspace in cloud mode, so there is no OS file to watch — bridge.pins.watchFile / unwatchFile / onFileChanged are no-ops there (and have no renderer consumer; they predate the frameset pin model). A caller that needs to observe pin-file changes under cloud workspace mode should subscribe to bridge.workspaceFiles.subscribe with a pins/ prefix instead, which reports local and remote changes alike. The commands below describe the local-engine (Tauri desktop invoke) behavior.
pins_watch_file
Start watching a single pin file for external modifications.
const success = await invoke<boolean>('pins_watch_file', {
pinIndex: 0,
entryPath: 'notes/daily.md',
});| Parameter | Type | Description |
|---|---|---|
pinIndex | number | Zero-based index (reserved for future use) |
entryPath | string | Relative path from project root to watch |
Returns: Result<boolean, string> — true on success.
Behavior:
Stops any existing pin watcher before starting a new one (only one pin file can be watched at a time)
Returns an error if the file doesn't exist
See the File Watchers page for debounce and mtime details
pins_unwatch_file
Stop watching the current pin file.
const success = await invoke<boolean>('pins_unwatch_file');Returns: Result<boolean, string> — true on success.
Event
pins:fileChanged
Emitted when the watched pin file is modified externally.
await listen<{ entryPath: string }>('pins:fileChanged', (event) => {
console.log('Pin file changed:', event.payload.entryPath);
});| Payload field | Type | Description |
|---|---|---|
entryPath | string | The relative path that was passed to pins_watch_file |