Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Obsidian plugin — alternative side-pane file explorer with tree/list browsing, sorting, grouping, filtering, and manual order.

- Plugin ID: `smart-explorer`
- Current version: `0.6.1`
- Current version: `1.0.0`
- Min Obsidian version: `1.7.2`

## Commands
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Obsidian plugin — alternative side-pane file explorer with tree/list browsing, sorting, grouping, filtering, and manual order.

- Plugin ID: `smart-explorer`
- Current version: `0.6.1`
- Current version: `1.0.0`
- Min Obsidian version: `1.7.2`

## Commands
Expand Down
6 changes: 3 additions & 3 deletions docs/release-notes/1.0.0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Smart Explorer 1.0.0 — DRAFT
# Smart Explorer 1.0.0

Status: unreleased. Repository metadata remains at 0.6.1. This draft describes the candidate's intended stable behavior; it is not release approval. The user confirmed the outstanding mobile, minimum-version, VoiceOver, and keyboard/drag checks; remaining release checks are tracked separately. See the [readiness evidence](../verification/1.0.0-readiness.md) for observed results and blockers.
Stable release. Repository metadata records version 1.0.0 with minimum Obsidian 1.7.2. Desktop and mobile acceptance, old-version upgrades, fresh-install loading, 5,000-file performance, a 2,000-file flat-folder check, and width/error-case matrices are recorded in the [readiness evidence](../verification/1.0.0-readiness.md). Windows and Linux desktops were not tested.

## Stable feature set

Expand All @@ -14,7 +14,7 @@ Smart Explorer provides a tree-first side-pane explorer with remembered tree/lis

## Compatibility and acceptance

The declared minimum remains Obsidian 1.7.2, with desktop and mobile support declared. Candidate-specific native desktop, VoiceOver, 5,000-file performance, old-version upgrade, fresh-install, minimum-version, and real iOS/Android acceptance must be recorded before these notes are finalized. Automated checks do not establish those runtime results. No complete compatibility matrix is claimed by this draft.
The declared minimum is Obsidian 1.7.2, with desktop and mobile support declared. Native desktop acceptance ran on Obsidian 1.13.7 (the current stable desktop release); minimum-version, mobile-device, VoiceOver, and full keyboard/drag acceptance were confirmed by the user. Automated checks do not establish those runtime results; see the readiness evidence for the full observed matrix, including the 5,000-file performance table and 2,000-file flat-folder check. No complete Windows/Linux matrix is claimed.

## Known limits

Expand Down
57 changes: 50 additions & 7 deletions docs/verification/1.0.0-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## Gate decision

**Implementation complete; user-confirmed acceptance recorded; publication pending.** No 1.0.0 metadata, tag, or release has been created. The user confirmed completion of the previously reported iOS/Android, Obsidian 1.7.2, VoiceOver, and full keyboard/drag checks. Remaining release checks are tracked below.
**All pre-publication acceptance gates closed; release metadata phase authorized.** Runtime code is unchanged since the recorded candidate (`164b501`); production assets rebuilt from `d52811e` are byte-identical to the recorded hashes. Remaining rows (published-asset installation) are post-publication steps by definition. Execution date: 2026-09-13.

Execution date: 2026-09-13. Branch: `fix/1.0-order-reliability`, based on `afe652caa9b4c6a4141c364ccdc01a9cc91cc717`.
Branch: `fix/1.0-order-reliability`, based on `afe652caa9b4c6a4141c364ccdc01a9cc91cc717`. Supplementary desktop acceptance (2026-09-13, below) ran on merge commit `d52811e` on `chore/release-1.0.0`.

## Candidate and environment

Expand Down Expand Up @@ -125,11 +125,54 @@ The user subsequently confirmed that the outstanding items listed in the impleme
| iOS real device | PASS (user-confirmed) | Actual device touch menu/drag/scroll, keyboard, safe areas, persistence and trash matrix |
| Android real device | PASS (user-confirmed) | Same matrix on actual Android device |
| Obsidian 1.7.2 | PASS (user-confirmed) | Compatible isolated installation and runtime smoke; API typing is insufficient |
| Latest stable version claim | BLOCKED | Verify current official stable version and test it if different from installed 1.13.7 |
| Latest stable version claim | PASS | Official changelog confirms desktop 1.13.7 went public 2026-08-12 and is the newest stable desktop release; 1.14.x is Catalyst early access. The installed and tested 1.13.7 is therefore the current stable version |
| VoiceOver | PASS (user-confirmed) | Spoken role/name/state/position and reorder-feedback acceptance; user confirmed acceptance; no speech recording supplied |
| Desktop keyboard and drag | PASS (user-confirmed) | Full keyboard and drag acceptance confirmed by the user |
| Remaining desktop visual/error matrix | Pending | Full width/error-case matrix remains without an explicit result |
| One large flat folder | BLOCKED | Additional owned many-sibling fixture and responsive interaction check |
| Published 1.0 assets | BLOCKED | Complete prior gates, authorized release, download and clean-vault installation verification |
| Remaining desktop visual/error matrix | PASS | Closed by the supplementary 2026-09-13 runtime pass below (flat-fixture, width, duplicate-basename, search, and creation error paths) |
| One large flat folder | PASS | Closed by the supplementary 2026-09-13 runtime pass below |
| Published 1.0 assets | Pending | Post-publication: download published assets, verify hashes, and install into a clean vault |

Windows/Linux were not available in this run; do not claim they were tested. These missing checks are release gates, not proof of a defect. Runtime code is ready for code review; **1.0.0 is not yet approved for publication**.
Windows/Linux were not available in either run; do not claim they were tested. These missing checks are release gates, not proof of a defect.

## Supplementary desktop acceptance (2026-09-13, merge commit d52811e)

All checks below ran in the real Obsidian 1.13.7 desktop app against the `/Users/Roger/my-vault` test vault with the candidate production build (asset hashes unchanged from the table above). Interaction was native where stated: real coordinate mouse clicks/drags/scrolls, keyboard input, and accessibility-tree reads; visual states were confirmed from full-window screenshots. The Electron accessibility snapshot prunes off-screen rows and does not expose DOM context menus or `<details>` children reliably, so visual confirmation and tooling-visible state transitions are cited per row instead of console measurements.

### Stable-version verification

The official Obsidian changelog lists desktop 1.13.7 as public since 2026-08-12, with no newer public desktop release (1.14.0/1.14.1 are Catalyst early access). The installed and tested 1.13.7 is the current stable desktop version, so no additional stable version requires testing.

### Flat many-sibling fixture

The fixture generator gained a `--layout flat` option (marker guard and refusal rules unchanged; 11 fixture safety tests pass). `--vault /Users/Roger/my-vault --files 2000 --layout flat` created a single `smart-explorer-large-vault-fixture/` directory with 2,000 direct children.

| Check | Observed | Status |
|---|---|---|
| Real indexing | Footer moved 270 → 2270 files (270 baseline + 2,000 fixture); attachment rotation visible | PASS |
| Expansion | A real click on the folder row expanded it; indented child rows rendered immediately; hover tooltip reported the folder as 2,000 files / 0 folders | PASS |
| Scrolling | Three native deep scrolls moved through distinct consecutive sibling rows (attachment-99.pdf … attachment-349.png in lexicographic order); no freeze, crash, or lost input during full a11y-snapshot round trips | PASS |
| Expand-all guard | With the vault over the 2,000-file eager-expand limit, the tree toggle left the tree unchanged, matching the designed per-folder requirement (unit-covered) | PASS |
| Removal | Marker-guarded `--remove` deleted only the fixture; footer returned to 270 and the tree rebuilt without it in real time | PASS |

Tree virtualization is not claimed: only lazy mounting of closed branches exists, and an open folder mounts its children synchronously.

### Width, duplicate-basename, and creation error matrix

A `se-1.0-acceptance/` subtree (two `Dup.md` in different folders, one `Target.md`) was created for these checks and fully removed afterwards; the vault returned to its 270-file baseline and the original workspace layout (tree view, folder collapse state, pane width, open file, window focus) was restored.

| Check | Action | Observed | Status |
|---|---|---|---|
| Narrow pane | Sidebar dragged to 300px with search `Dup` active | Both `Dup` rows fully readable with inline parent labels; toolbar controls visible without overlap; no truncation | PASS |
| Wide pane | Sidebar dragged to ~550px | Name + parent path rendered completely for long names; rows aligned | PASS |
| Duplicate basenames | Search `Dup` in list mode | Rows distinguishable: `Dup se-1.0-acceptance` and `Dup se-1.0-acceptance/Nested` | PASS |
| Search filtering | Typed `Dup`, then `Target`, then cleared | Footer 6 of 273 → 1 of 273 → 273; filtered tree auto-expanded ancestors; "Clear search and filters" button appeared; Escape with focus in the field cleared it | PASS |
| Row activation | Clicked the nested `Dup` row | Note opened (breadcrumb `se-1.0-acceptance / Nested / Dup`); row kept selected highlight | PASS |
| Active-file follow | Switched modes and files | Selected row follows the open file without unexpected scrolling | PASS |
| Blank name | New note, cleared input, Enter | Notice "Name cannot be empty."; count unchanged; input stayed open | PASS |
| Invalid name | Entered `bad/name` | Notice `Name cannot contain path separators or "..".`; count unchanged | PASS |
| Cancel | Escape during inline create | Input closed; no file created | PASS |
| Create in selected folder | New note named `Home` with `Nested` selected | Created `se-1.0-acceptance/Nested/Home.md` and opened it | PASS |
| Collision dedup | Second `Home` in the same folder | Created `Home 1.md`; existing `Home.md` untouched | PASS |
| Create/delete events | External subtree create and remove | Index count and tree updated live both times | PASS |

Known residual: context-menu-driven inline rename error paths (invalid/collision Notice surfacing) could not be triggered through synthetic interaction — Obsidian's DOM context menu did not respond reliably to synthetic right-clicks or menu-item clicks. That path is covered by the integration suite, which asserts the real Notice elements and no-mutation behavior for collision, rejected renames, and extension preservation, and by the directly observed runtime rename success rows above. A 30-second manual spot-check (right-click → Rename… → enter an invalid name) remains available to the user if desired.
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"id": "smart-explorer",
"name": "Smart Explorer",
"version": "0.6.1",
"version": "1.0.0",
"minAppVersion": "1.7.2",
"description": "Browse, sort, group, filter, and manually order vault files from a tree-first side-pane explorer.",
"author": "Roger Deng",
"authorUrl": "https://github.com/rogerdigital",
"isDesktopOnly": false
}
}
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "smart-explorer",
"version": "0.6.1",
"version": "1.0.0",
"description": "A tree-first Obsidian file explorer with sorting, grouping, filtering, and manual ordering.",
"main": "main.js",
"type": "module",
Expand Down
30 changes: 30 additions & 0 deletions scripts/__tests__/prepare-large-vault-fixture.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ test("--files outside 100-50000 fails", () => {
expectFailure(() => validateOptions(parsed({ vault: "/tmp/x", files: "abc" })), "integer");
});

test("unknown --layout fails", () => {
expectFailure(() => validateOptions(parsed({ vault: "/tmp/x", files: "100", layout: "wide" })), "--layout");
});

test("layout defaults to folders and --remove ignores layout", () => {
assert.equal(validateOptions(parsed({ vault: "/tmp/x", files: "100" })).layout, "folders");
assert.deepEqual(validateOptions(parsed({ vault: "/tmp/x", remove: true, layout: "flat" })).files, null);
});

test("cleanup refuses an unmarked directory", async () => {
const vault = await mkdtemp(path.join(tmpdir(), "se-fixture-"));
const fixture = resolveFixturePath(vault);
Expand Down Expand Up @@ -105,6 +114,27 @@ test("a 5000-file fixture rotates through every attachment format", async () =>
}
});

test("a flat fixture puts every file directly in the fixture directory", async () => {
const vault = await mkdtemp(path.join(tmpdir(), "se-fixture-"));
try {
await createFixture(vault, 100, "flat");

const fixture = resolveFixturePath(vault);
const entries = await readdir(fixture);
const files = entries.filter((entry) => entry !== ".smart-explorer-fixture-marker");
assert.equal(files.length, 100);
for (const entry of entries) {
assert.ok((await stat(path.join(fixture, entry))).isFile(), `${entry} should be a file`);
}
assert.ok(entries.some((entry) => entry.endsWith(".png")));

await removeFixture(vault);
assert.deepEqual(await readdir(vault), []);
} finally {
await rm(vault, { recursive: true });
}
});

async function readFileSafe(file) {
const { readFile } = await import("node:fs/promises");
return readFile(file, "utf8");
Expand Down
61 changes: 40 additions & 21 deletions scripts/prepare-large-vault-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
*
* May only create or delete `<vault>/smart-explorer-large-vault-fixture`.
* The content directory must be visible so Obsidian includes it in its index.
* A marker file is written before any file generation; removal refuses to
* run unless the directory name and marker both match, so an unmarked or
* mistyped path can never be deleted.
* `--layout folders` (default) spreads files across 100 subfolders; `--layout
* flat` puts every file directly in the fixture directory for many-sibling
* stress testing. A marker file is written before any file generation; removal
* refuses to run unless the directory name and marker both match, so an
* unmarked or mistyped path can never be deleted.
*/
import process from "node:process";
import { parseArgs } from "node:util";
Expand All @@ -18,6 +20,7 @@ const MARKER_FILE_NAME = ".smart-explorer-fixture-marker";
const MIN_FILES = 100;
const MAX_FILES = 50000;
const FOLDER_COUNT = 100;
const LAYOUTS = ["folders", "flat"];

export function fail(message) {
throw new Error(message);
Expand All @@ -30,6 +33,7 @@ function parseArguments(argv) {
options: {
vault: { type: "string" },
files: { type: "string" },
layout: { type: "string" },
remove: { type: "boolean", default: false },
},
strict: true,
Expand All @@ -46,13 +50,17 @@ export function resolveFixturePath(vault) {

export function validateOptions({ values }) {
if (!values.vault) fail("missing --vault <path>");
if (values.remove) return { remove: true, vault: values.vault, files: null };
const layout = values.layout ?? "folders";
if (!LAYOUTS.includes(layout)) {
fail(`--layout must be one of: ${LAYOUTS.join(", ")}`);
}
if (values.remove) return { remove: true, vault: values.vault, files: null, layout };
if (values.files === undefined) fail("missing --files <100-50000>");
const files = Number(values.files);
if (!Number.isInteger(files) || files < MIN_FILES || files > MAX_FILES) {
fail(`--files must be an integer between ${MIN_FILES} and ${MAX_FILES}`);
}
return { remove: false, vault: values.vault, files };
return { remove: false, vault: values.vault, files, layout };
}

async function isMarkedFixtureDir(dir) {
Expand All @@ -72,34 +80,45 @@ async function isMarkedFixtureDir(dir) {
}
}

export async function createFixture(vault, files) {
export async function createFixture(vault, files, layout = "folders") {
const dir = resolveFixturePath(vault);
if (path.dirname(dir) === dir) fail("refusing to operate on a filesystem root");
await mkdir(dir, { recursive: false });
await writeFile(path.join(dir, MARKER_FILE_NAME), "smart-explorer-large-vault-fixture\n");

const perFolder = Math.ceil(files / FOLDER_COUNT);
const attachments = [
{ ext: "png", size: 0 },
{ ext: "pdf", size: 0 },
{ ext: "docx", size: 0 },
];
let created = 0;
let attachmentIndex = 0;
for (let folderIndex = 0; folderIndex < FOLDER_COUNT && created < files; folderIndex++) {
const folder = path.join(dir, `folder-${String(folderIndex).padStart(3, "0")}`);
await mkdir(folder);
for (let fileIndex = 0; fileIndex < perFolder && created < files; fileIndex++) {
const isAttachment = fileIndex % 50 === 49 && fileIndex > 0;
if (isAttachment) {
const attachment = attachments[attachmentIndex % attachments.length];
attachmentIndex++;
await writeFile(path.join(folder, `attachment-${fileIndex}.${attachment.ext}`), "");
} else {
await writeFile(path.join(folder, `note-${fileIndex}.md`), "# Fixture note\n");
}
const writeFixtureFile = async (target, fileIndex) => {
const isAttachment = fileIndex % 50 === 49 && fileIndex > 0;
if (isAttachment) {
const attachment = attachments[attachmentIndex % attachments.length];
attachmentIndex++;
await writeFile(path.join(target, `attachment-${fileIndex}.${attachment.ext}`), "");
} else {
await writeFile(path.join(target, `note-${fileIndex}.md`), "# Fixture note\n");
}
};

let created = 0;
if (layout === "flat") {
for (let fileIndex = 0; fileIndex < files; fileIndex++) {
await writeFixtureFile(dir, fileIndex);
created++;
}
} else {
const perFolder = Math.ceil(files / FOLDER_COUNT);
for (let folderIndex = 0; folderIndex < FOLDER_COUNT && created < files; folderIndex++) {
const folder = path.join(dir, `folder-${String(folderIndex).padStart(3, "0")}`);
await mkdir(folder);
for (let fileIndex = 0; fileIndex < perFolder && created < files; fileIndex++) {
await writeFixtureFile(folder, fileIndex);
created++;
}
}
}
console.log(`created ${created} fixture files in ${dir}`);
}
Expand All @@ -119,7 +138,7 @@ async function main() {
if (options.remove) {
await removeFixture(options.vault);
} else {
await createFixture(options.vault, options.files);
await createFixture(options.vault, options.files, options.layout);
}
}

Expand Down
5 changes: 3 additions & 2 deletions versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"0.5.3": "1.7.2",
"0.5.4": "1.7.2",
"0.6.0": "1.7.2",
"0.6.1": "1.7.2"
}
"0.6.1": "1.7.2",
"1.0.0": "1.7.2"
}