0.133.0: closing a Stashpad-opened tab returns to the tab you came from (search results + aggregate/trash views)

This commit is contained in:
Human 2026-07-01 22:24:04 -07:00
parent c2a3567e34
commit b5afc87d38
8 changed files with 150 additions and 140 deletions

146
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"id": "stashpad",
"name": "Stashpad",
"version": "0.132.0",
"version": "0.133.0",
"minAppVersion": "1.13.0",
"description": "A chat-style, nested-notes workspace: rapid capture, outliner navigation, fast search, tasks, and per-folder templates, with one-click Open Knowledge Format (OKF) export for LLMs and agents.",
"author": "Human",

View file

@ -1,6 +1,6 @@
{
"name": "stashpad-obsidian",
"version": "0.132.0",
"version": "0.133.0",
"private": true,
"scripts": {
"dev": "node esbuild.config.mjs",

17
release-notes/0.133.0.md Normal file
View file

@ -0,0 +1,17 @@
# 0.133.0 — Closing a Stashpad-opened tab returns you to the tab you came from
Opening a note in the editor already returned focus to the originating Stashpad
tab when that editor tab was closed. This release makes the same behavior
universal: **search results and the aggregate views now do it too.**
- **Search results** — opening a search hit in a new tab and then closing it now
returns you to the tab you searched from, instead of Obsidian's default of
jumping to the tab on the right.
- **Aggregate views** — the same return-to-origin behavior on close for the
**All encrypted**, **All archived**, and **All tasks** views, plus the **Trash**
view. Works whether the view was opened from a Stashpad tab or the command
palette.
Internally, the return-to-origin logic (previously duplicated across the
editor-open, new-Stashpad-tab, and folder-open paths) is now a single shared
helper, so every current and future tab-opener behaves consistently.

View file

@ -3,6 +3,7 @@ import type StashpadPlugin from "./main";
import { STASHPAD_AGGREGATE_VIEW_TYPE } from "./types";
import { renderTaskTriage, defaultTaskTriageState, type TaskTriageState } from "./task-render";
import { renderAggModeBar, type AggMode } from "./agg-modes";
import { returnToOriginOnClose } from "./leaf-return";
// Obsidian types `moment` as a namespace (not callable); cast to a callable.
const momentFn = moment as unknown as (...args: unknown[]) => { fromNow: () => string };
@ -256,7 +257,11 @@ export async function openAggregateView(plugin: StashpadPlugin, mode: AggregateM
const existing = workspace.getLeavesOfType(STASHPAD_AGGREGATE_VIEW_TYPE)
.find((l) => (l.view as StashpadAggregateView)?.getState?.().mode === mode);
if (existing) { workspace.revealLeaf(existing); return; }
// 0.133.0: remember the tab we opened from so closing this aggregate view
// returns there, not to the tab on the right.
const originLeaf = workspace.getMostRecentLeaf();
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({ type: STASHPAD_AGGREGATE_VIEW_TYPE, active: true, state: { mode } });
workspace.revealLeaf(leaf);
returnToOriginOnClose(workspace, leaf, originLeaf);
}

36
src/leaf-return.ts Normal file
View file

@ -0,0 +1,36 @@
import type { Workspace, WorkspaceLeaf, EventRef } from "obsidian";
/** Restore focus to `originLeaf` when `newLeaf` closes, instead of letting
* Obsidian fall back to the tab on the right. One-shot: the listener detaches
* itself the moment the spawned leaf is gone.
*
* Shared by every Stashpad new-tab opener edit-in-editor, "open in new
* Stashpad tab", search-in-context, and the aggregate / trash / archive /
* tasks views so "close this tab, land back where I came from" is universal.
*
* No-ops when there's no distinct origin (origin missing, or identical to the
* spawned leaf) e.g. an aggregate view opened from the command palette with
* nothing else in the main area. In that case Obsidian's default stands. */
export function returnToOriginOnClose(
ws: Workspace,
newLeaf: WorkspaceLeaf,
originLeaf: WorkspaceLeaf | null,
): void {
if (!originLeaf || originLeaf === newLeaf) return;
const isOpen = (target: WorkspaceLeaf): boolean => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === target) found = true; });
return found;
};
const off: EventRef = ws.on("active-leaf-change", () => {
// Spawned tab still around (user just switched away from it) — leave it.
if (isOpen(newLeaf)) return;
// Spawned tab closed. Detach this listener and, if the origin tab is still
// open, make it active instead of whatever Obsidian picked (the right tab).
ws.offref(off);
if (isOpen(originLeaf)) {
ws.setActiveLeaf(originLeaf, { focus: true });
ws.revealLeaf(originLeaf);
}
});
}

View file

@ -4,6 +4,7 @@ import { STASHPAD_TRASH_VIEW_TYPE } from "./types";
import { ConfirmModal } from "./modals";
import type { DeletedMeta } from "./encryption-ops";
import { renderAggModeBar, type AggMode } from "./agg-modes";
import { returnToOriginOnClose } from "./leaf-return";
// Obsidian types `moment` as the namespace (not callable); cast to a callable.
const momentFn = moment as unknown as (...args: unknown[]) => { fromNow: () => string };
@ -288,7 +289,10 @@ export async function openTrashView(plugin: StashpadPlugin): Promise<void> {
const { workspace } = plugin.app;
const existing = workspace.getLeavesOfType(STASHPAD_TRASH_VIEW_TYPE);
if (existing.length > 0) { workspace.revealLeaf(existing[0]); return; }
// 0.133.0: closing the trash view returns to the tab it was opened from.
const originLeaf = workspace.getMostRecentLeaf();
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({ type: STASHPAD_TRASH_VIEW_TYPE, active: true });
workspace.revealLeaf(leaf);
returnToOriginOnClose(workspace, leaf, originLeaf);
}

View file

@ -28,6 +28,7 @@ import { matchBinding, humanCombo } from "./view-keys";
import { AuthorshipTracker } from "./authorship-tracker";
import { ViewDnD } from "./view-dnd";
import { NoteBodyRenderer } from "./note-body-renderer";
import { returnToOriginOnClose } from "./leaf-return";
import { computeSortedIds } from "./view-sort";
import {
SHEET_KEY,
@ -8966,27 +8967,9 @@ export class StashpadView extends ItemView {
});
ws.setActiveLeaf(leaf, { focus: true });
ws.revealLeaf(leaf);
// 0.57.5: same return-to-origin one-shot as openFolderInNewTab /
// openFileAtEnd — when this spawned tab closes, the originating
// Stashpad tab regains focus.
const off = ws.on("active-leaf-change", () => {
const stillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === leaf) found = true; });
return found;
})();
if (stillOpen) return;
ws.offref(off);
const originStillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === originLeaf) found = true; });
return found;
})();
if (originStillOpen) {
ws.setActiveLeaf(originLeaf, { focus: true });
ws.revealLeaf(originLeaf);
}
});
// 0.57.5: when this spawned tab closes, the originating Stashpad tab
// regains focus (see returnToOriginOnClose — shared by every opener).
returnToOriginOnClose(ws, leaf, originLeaf);
}
/** Open a Stashpad folder's home in a new tab (any folder, not just
@ -9006,6 +8989,7 @@ export class StashpadView extends ItemView {
if (!cleaned || !noteId) return;
const settingsFolder = (this.plugin.settings.folder || "Stashpad").trim().replace(/^\/+|\/+$/g, "") || "Stashpad";
const ws = this.app.workspace;
const originLeaf = this.leaf;
const leaf = ws.getLeaf("tab");
await leaf.setViewState({
type: STASHPAD_VIEW_TYPE,
@ -9020,6 +9004,9 @@ export class StashpadView extends ItemView {
});
ws.setActiveLeaf(leaf, { focus: true });
ws.revealLeaf(leaf);
// 0.133.0: closing the search-opened tab returns to the tab you searched
// from, not the tab to the right.
returnToOriginOnClose(ws, leaf, originLeaf);
}
private async openFolderInNewTab(folder: string): Promise<void> {
@ -9041,27 +9028,8 @@ export class StashpadView extends ItemView {
});
ws.setActiveLeaf(leaf, { focus: true });
ws.revealLeaf(leaf);
// One-shot: when the spawned leaf closes, restore focus to the
// originating Stashpad tab.
const off = ws.on("active-leaf-change", () => {
const stillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === leaf) found = true; });
return found;
})();
if (stillOpen) return;
ws.offref(off);
const originStillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === originLeaf) found = true; });
return found;
})();
if (originStillOpen) {
ws.setActiveLeaf(originLeaf, { focus: true });
ws.revealLeaf(originLeaf);
}
});
// When the spawned leaf closes, restore focus to the originating tab.
returnToOriginOnClose(ws, leaf, originLeaf);
}
// --- Open shortcuts ---
@ -9103,29 +9071,9 @@ export class StashpadView extends ItemView {
ws.setActiveLeaf(leaf, { focus: true });
ws.revealLeaf(leaf);
// One-shot listener: when the active leaf changes AND our edit leaf is
// no longer in the workspace (closed), reveal the originating Stashpad
// leaf instead of whatever Obsidian picked.
const off = ws.on("active-leaf-change", () => {
const stillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === leaf) found = true; });
return found;
})();
if (stillOpen) return;
// Edit leaf is gone. Detach this listener and (if the origin leaf
// is still around) make it active.
ws.offref(off);
const originStillOpen = (() => {
let found = false;
ws.iterateAllLeaves((l) => { if (l === originLeaf) found = true; });
return found;
})();
if (originStillOpen) {
ws.setActiveLeaf(originLeaf, { focus: true });
ws.revealLeaf(originLeaf);
}
});
// When the edit tab closes, reveal the originating Stashpad leaf instead
// of whatever Obsidian picked (the tab to the right).
returnToOriginOnClose(ws, leaf, originLeaf);
const view: any = leaf.view;
const editor: any = view?.editor;