From 7740154bdce50b38c5d74c7ad7d2aeb608701212 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 14:32:53 +0800 Subject: [PATCH 01/27] feat(window): add windowId for extra windows --- apps/client/src/components/app_context.ts | 7 +-- apps/client/src/components/entrypoints.ts | 5 +- apps/client/src/types.d.ts | 1 + apps/desktop/src/main.ts | 5 +- .../assets/views/partials/windowGlobal.ejs | 1 + apps/server/src/routes/index.ts | 1 + apps/server/src/services/window.ts | 54 +++++++++++++------ 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index e0b8c651b..aba2920d8 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -535,6 +535,7 @@ export type FilteredCommandNames = keyof Pick | (() => boolean))[]; tabManager!: TabManager; @@ -543,10 +544,11 @@ export class AppContext extends Component { lastSearchString?: string; - constructor(isMainWindow: boolean) { + constructor(isMainWindow: boolean, windowId: string) { super(); this.isMainWindow = isMainWindow; + this.windowId = windowId; // non-widget/layout components needed for the application this.components = []; this.beforeUnloadListeners = []; @@ -676,8 +678,7 @@ export class AppContext extends Component { this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener); } } - -const appContext = new AppContext(window.glob.isMainWindow); +const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId); // we should save all outstanding changes before the page/app is closed $(window).on("beforeunload", () => { diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 8a902666f..4c67dce2a 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -142,14 +142,15 @@ export default class Entrypoints extends Component { } async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) { + const extraWindowId = utils.randomString(4); const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope }); if (utils.isElectron()) { const { ipcRenderer } = utils.dynamicRequire("electron"); - ipcRenderer.send("create-extra-window", { extraWindowHash }); + ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash }); } else { - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`; + const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`; window.open(url, "", "width=1000,height=800"); } diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index 7128ea5d8..34cd9a8fe 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -34,6 +34,7 @@ interface CustomGlobals { isProtectedSessionAvailable: boolean; isDev: boolean; isMainWindow: boolean; + windowId: string; maxEntityChangeIdAtLoad: number; maxEntityChangeSyncIdAtLoad: number; assetPath: string; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3617c4c9f..ecbbd2024 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,6 +6,8 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js"; import windowService from "@triliumnext/server/src/services/window.js"; import tray from "@triliumnext/server/src/services/tray.js"; import options from "@triliumnext/server/src/services/options.js"; +import { randomString } from "@triliumnext/server/src/services/utils.js"; + import electronDebug from "electron-debug"; import electronDl from "electron-dl"; import { PRODUCT_NAME } from "./app-info"; @@ -72,7 +74,8 @@ async function main() { app.on("second-instance", (event, commandLine) => { const lastFocusedWindow = windowService.getLastFocusedWindow(); if (commandLine.includes("--new-window")) { - windowService.createExtraWindow(""); + const extraWindowId = randomString(4); + windowService.createExtraWindow(extraWindowId, ""); } else if (lastFocusedWindow) { if (lastFocusedWindow.isMinimized()) { lastFocusedWindow.restore(); diff --git a/apps/server/src/assets/views/partials/windowGlobal.ejs b/apps/server/src/assets/views/partials/windowGlobal.ejs index c69351e1a..382990712 100644 --- a/apps/server/src/assets/views/partials/windowGlobal.ejs +++ b/apps/server/src/assets/views/partials/windowGlobal.ejs @@ -12,6 +12,7 @@ isDev: <%= isDev %>, appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>, isMainWindow: <%= isMainWindow %>, + windowId: "<%= windowId %>", isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>, triliumVersion: "<%= triliumVersion %>", assetPath: "<%= assetPath %>", diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index cde9885f0..4491764f3 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -56,6 +56,7 @@ function index(req: Request, res: Response) { appCssNoteIds: getAppCssNoteIds(), isDev, isMainWindow: view === "mobile" ? true : !req.query.extraWindow, + windowId: view !== "mobile" && req.query.extraWindow ? req.query.extraWindow : "main", isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(), triliumVersion: packageJson.version, assetPath, diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index 4431226ab..7670b310d 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -16,28 +16,47 @@ import { RESOURCE_DIR } from "./resource_dir.js"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; let setupWindow: BrowserWindow | null; -let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus. -function trackWindowFocus(win: BrowserWindow) { +interface WindowEntry { + window: BrowserWindow; + windowId: string; // custom window ID +} +let allWindowEntries: WindowEntry[] = []; + +function trackWindowFocus(win: BrowserWindow, windowId: string) { // We need to get the last focused window from allWindows. If the last window is closed, we return the previous window. // Therefore, we need to push the window into the allWindows array every time it gets focused. win.on("focus", () => { - allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win); - allWindows.push(win); + allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win); + allWindowEntries.push({ window: win, windowId: windowId }); + if (!optionService.getOptionBool("disableTray")) { electron.ipcMain.emit("reload-tray"); } }); win.on("closed", () => { - allWindows = allWindows.filter(w => !w.isDestroyed()); + cls.wrap(() => { + const savedWindows = JSON.parse( + optionService.getOption("openNoteContexts") || "[]" + ); + + const win = savedWindows.find(w => w.windowId === windowId); + if (win) { + win.closedAt = Date.now(); + } + + optionService.setOption("openNoteContexts", JSON.stringify(savedWindows)); + })(); + + allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed()); if (!optionService.getOptionBool("disableTray")) { electron.ipcMain.emit("reload-tray"); } }); } -async function createExtraWindow(extraWindowHash: string) { +async function createExtraWindow(extraWindowId: string, extraWindowHash: string) { const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); const { BrowserWindow } = await import("electron"); @@ -56,15 +75,15 @@ async function createExtraWindow(extraWindowHash: string) { }); win.setMenuBarVisibility(false); - win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`); + win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`); configureWebContents(win.webContents, spellcheckEnabled); - trackWindowFocus(win); + trackWindowFocus(win, extraWindowId); } electron.ipcMain.on("create-extra-window", (event, arg) => { - createExtraWindow(arg.extraWindowHash); + createExtraWindow(arg.extraWindowId, arg.extraWindowHash); }); interface PrintOpts { @@ -168,8 +187,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac return browserWindow; } -async function createMainWindow(app: App) { - if ("setUserTasks" in app) { +async function createMainWindow(app?: App) { + if (app && "setUserTasks" in app) { app.setUserTasks([ { program: process.execPath, @@ -219,7 +238,7 @@ async function createMainWindow(app: App) { mainWindow.on("closed", () => (mainWindow = null)); configureWebContents(mainWindow.webContents, spellcheckEnabled); - trackWindowFocus(mainWindow); + trackWindowFocus(mainWindow, "main"); } function getWindowExtraOpts() { @@ -381,11 +400,15 @@ function getMainWindow() { } function getLastFocusedWindow() { - return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null; + return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null; } function getAllWindows() { - return allWindows; + return allWindowEntries.map(e => e.window); +} + +function getAllWindowIds(): string[] { + return allWindowEntries.map(e => e.windowId); } export default { @@ -396,5 +419,6 @@ export default { registerGlobalShortcuts, getMainWindow, getLastFocusedWindow, - getAllWindows + getAllWindows, + getAllWindowIds }; From 3353d4f436b189a986c8904e9db02b1e17f51852 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 14:33:34 +0800 Subject: [PATCH 02/27] feat(window): record openNoteContents of recently closed windows --- apps/client/src/components/tab_manager.ts | 52 ++++++++++++++++++++--- apps/server/src/services/options_init.ts | 22 ++++++++-- apps/server/src/services/sql_init.ts | 11 ++++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 3cf06a779..ee00cf0cb 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -41,9 +41,6 @@ export default class TabManager extends Component { this.recentlyClosedTabs = []; this.tabsUpdate = new SpacedUpdate(async () => { - if (!appContext.isMainWindow) { - return; - } if (options.is("databaseReadonly")) { return; } @@ -52,9 +49,21 @@ export default class TabManager extends Component { .map((nc) => nc.getPojoState()) .filter((t) => !!t); - await server.put("options", { - openNoteContexts: JSON.stringify(openNoteContexts) - }); + // Update the current window’s openNoteContexts in options + const savedWindows = options.getJson("openNoteContexts"); + const win = savedWindows.find(w => w.windowId === appContext.windowId); + if (win) { + win.contexts = openNoteContexts; + } else { + savedWindows.push({ + windowId: appContext.windowId, + createdAt: Date.now(), + closedAt: null, + contexts: savedWindows + }); + } + + await options.save("openNoteContexts", JSON.stringify(savedWindows)); }); appContext.addBeforeUnloadListener(this); @@ -69,8 +78,13 @@ export default class TabManager extends Component { } async loadTabs() { + // Get the current window’s openNoteContexts + const savedWindows = options.getJson("openNoteContexts"); + const currentWin = savedWindows.find(w => w.windowId === appContext.windowId); + const openNoteContexts = currentWin ? currentWin.contexts : undefined; + try { - const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; + const noteContextsToOpen = openNoteContexts || []; // preload all notes at once await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) => @@ -119,6 +133,30 @@ export default class TabManager extends Component { } }); + // Save window contents + if (currentWin) { + currentWin.createdAt = Date.now(), + currentWin.closedAt = null; + currentWin.contexts = filteredNoteContexts; + } else { + // Filter out the oldest entry (excluding the main window) + if (savedWindows.length >= 10) { + const candidates = savedWindows.filter(w => w.windowId !== "main"); + const oldestIndex = candidates.reduce((a, b) => + a.createdAt < b.createdAt ? a : b + ); + savedWindows.splice(savedWindows.indexOf(oldestIndex), 1); + } + savedWindows.push({ + windowId: appContext.windowId, + createdAt: Date.now(), + closedAt: null, + contexts: filteredNoteContexts + }); + } + + await options.save("openNoteContexts", JSON.stringify(savedWindows)); + // if there's a notePath in the URL, make sure it's open and active // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window) if (parsedFromUrl.notePath) { diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index e8ef3c694..16dc93c55 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -45,8 +45,15 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = "openNoteContexts", JSON.stringify([ { - notePath: "root", - active: true + windowId: "main", + createdAt: 0, + closedAt: null, + contexts: [ + { + notePath: "root", + active: true + } + ] } ]), false @@ -251,8 +258,15 @@ function initStartupOptions() { "openNoteContexts", JSON.stringify([ { - notePath: process.env.TRILIUM_START_NOTE_ID || "root", - active: true + windowId: "main", + createdAt: 0, + closedAt: null, + contexts: [ + { + notePath: process.env.TRILIUM_START_NOTE_ID || "root", + active: true + } + ] } ]) ); diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index 93452669f..dc1adc11c 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -147,8 +147,15 @@ async function createInitialDatabase(skipDemoDb?: boolean) { "openNoteContexts", JSON.stringify([ { - notePath: startNoteId, - active: true + windowId: "main", + createdAt: 0, + closedAt: null, + contexts: [ + { + notePath: startNoteId, + active: true + } + ] } ]) ); From d7838f0b6700d56bf2f3a26308e08ba4e5a905db Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 14:37:35 +0800 Subject: [PATCH 03/27] feat(window): restore recently closed windows from tray --- .../tray/closed-windowsTemplate-inverted.png | Bin 0 -> 545 bytes .../closed-windowsTemplate-inverted@1.25x.png | Bin 0 -> 727 bytes .../closed-windowsTemplate-inverted@1.5x.png | Bin 0 -> 828 bytes .../closed-windowsTemplate-inverted@2x.png | Bin 0 -> 931 bytes .../images/tray/closed-windowsTemplate.png | Bin 0 -> 292 bytes .../tray/closed-windowsTemplate@1.25x.png | Bin 0 -> 355 bytes .../tray/closed-windowsTemplate@1.5x.png | Bin 0 -> 434 bytes .../images/tray/closed-windowsTemplate@2x.png | Bin 0 -> 492 bytes .../src/assets/translations/en/server.json | 2 + apps/server/src/services/tray.ts | 44 ++++++++++++++++++ 10 files changed, 46 insertions(+) create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@2x.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.25x.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate@1.5x.png create mode 100644 apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted.png new file mode 100644 index 0000000000000000000000000000000000000000..9e9671407838d9d196efa2b2566aca44bdf600be GIT binary patch literal 545 zcmV++0^a?JP)~K6g@MOm?kg27h(}V=q5ZDCAf6qq9UlP?%ftz5_tXsf6Ehe(M|t@tMWkFmEaFB zAiNZjg8IZb%4E8zW>b~IJFA&{=bU@)y#u@w1OWQ|{^xqV{wa#0m7*vb0O-1Y_s?TO z=yJJyHcj*Ga5#MQeg7)}AP9n|LZQH1*VP=yQ60xoFIB8mDmR5ffla5=?*KpnV6|HP z5<*DVb+h?=UN07l#wCGjwR+DvkJsz>lu{Bx2mqYU z^Z7iyy}iBjJg;XMMn(Yu&iP9ihM}S;Ob8(WV6j*%&NSS3Fin$oyWNk?X47uB+wVG^PR%sUMDVJElSre{u*c)^x6x?y zST2{}obn3rcgK`cFvcLIOh^Edq1v`>91e$_lrm1I(<*>nB0wo+aU5g6-=A$Im&;Mt zb?-gTd$24^tJP}mWHR}=*=!^MfWct!rPXRZ2q6xXQYNL00YC_GD3wYd2q9pMVYk}> j01U$b=N!jB_d5ImpO?StZgo@700000NkvXXu0mjfTi*Au literal 0 HcmV?d00001 diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.25x.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ee020b54f650a9edfc05184ac16ec8c421d864 GIT binary patch literal 727 zcmV;|0x127P)NklXLGaL?JP0|K9`up~atV77JZaBD z&tAM}4~39xDS;#`O_6@&LjoJqhS}`yOb=z#jY;g)KMV}Zd%t=A_x1tYbs_hXN~NM( zTU&1#V{JuIh-F#K=kxh7#?r7v&bia=b~}{Pz{$x;CY?@y>;qn2Ugp#3^y^Bca%GyP zJuxxiuh;9pOw)9R0V$=js;bKD?Ckyh{rz`yb8}As0Ax3y*=&B*b-lj0xcK1W;zD0v zUk@K09pxI01|J5Tnwr9~SnN@uQ21sT#t-*<52&i@Bq2o3=kpaI1e~9re zp+1-p0w|?8m&<*bo}N}!Rh=B@A!Dqi>-q=7Fn;=cK5E-`S5Xv#5W>SM6hd&*G=Gne zkI&4{&p#!E1o|qhR;!4q%Jx?+r74*(HDm{KZXi~(Z|qbhX!i4X$LIr#m4NtWe%13&-(&be?ww^{ZM zsaC6*$H&Ke13{%y>1=6fi2(fV&;MYfVn$~UfgprncX#*k#>U1B=NvXSH$yu+I}c^~ zpHX=f#48~LV2nWs0gXn(NF)+(mY0{$R##V_A08fRjx%JLN~O|SI2@)JV~}MT+~K&# zTdAgLFNF{yo6WwsX}@>loI9OP$KjlVZQH;(?>h(~5JEsa9uGw#k!U89c@~L8*5dJa z$UDHE<*~7`;L6HM2xI)65F&BTh37FS7K_Yr90&vg>cPQ5_Vo0$7K_D-gb>Aj48XH` z$z*bEdwYA&aU9FLZjvM^tyZfVjYeO!+il?{yZ@@&1{Zku^#^U4F{UW_xg!7o002ov JPDHLkV1jKwO?dzS literal 0 HcmV?d00001 diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate-inverted@1.5x.png new file mode 100644 index 0000000000000000000000000000000000000000..f0074a3cb894d9e2fe54cd634edfd585bdc45c1c GIT binary patch literal 828 zcmV-C1H=4@P)i9DwTq1nkB|qzFw~zhG7&LWBE>Enr5j|sT6cwKa57BkLD$}eZ`an= z-U9%5JRX|M+wY?2w_498Xg|LKR!Mlq?AH99F7hS z4t4_o5JK7pCxn2eX(3fry^Kdvh2Ya6952* zZBVP#$}1}?ulD!%KX-L?IV{WK2q6RjAcPQ|5)eYLX`1Jnrj4djsg0H`ibNvbPJuch z5{Y;lTjXfGq?9@dAwUQL%d&12*!H26Ha+R41e|kW2fl5BMjxls*=_4O4(~c(jN$i~ zKnMXCW6*W|Y<+z_dBvD9mYEbR zmzTr3t{-MHnK?yKP|J4x=?v28^a~+`I5;?XXX{$FNC?3RA=uu7c4Ain01yZS+|$$3 zZ+3Tg)2gZlQ>oO`w&WON`H6{%dz~2D?0h~S$!4=(GMUUA0KokG{1eNv>X&0&ELy!@ zH)dvLVv;0%aJgKPWm!#=LI?rpTx@M^WdQ)H)hbUU63=&bc1|$Hu)4bX_2}s6p|0yC zXoXlT*01aOVX0KgmCNOmR^MW=So|(YSN|V`P}@ED_wfr^)N32JYlkcV0000V4m229gzt*!%~ML@8U z`}_Ovi^XELo?+!KK*Sr7aUP9{F$STNciH|%EOijns(_F&R@nobbEw{jO%=d7hvDJj zua#0jL?EREB7#&Zg_dQt`o3R(W8X9d!Lknx4aJCvR##UaE2RJcps%m5rCcummCxs2 z#bPmbRVqG7K!BE(7O-u5nuus|aq)-2!NIRerP4dC_1XCNcpLyCDQKF2ATq-+V19o7 zQSb~al}hi%#>Vc1ZEi&Xz=!Di`}A)eU~O&f zCnBPyrKKO0Qjt0ld6F^4xYio9)(u|-?d|Qx!NEZ;olf6hUS2+6jKRdj#EXkNz*9=G zYQLA(BbUp)Or=uaZES2j!b<=E literal 0 HcmV?d00001 diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..e3adb5bd560de208419992c8c541c08a76442d6f GIT binary patch literal 292 zcmV+<0o(qGP)0+Kb#lMTA#Oaflu4 zt^gck5;b!RZ@9vH2JPT0%c=IGu-C^yBZA`yYz7@(e&}E;a>v384Dqz0?K%x*?Y0Vw znO)`_F#QAIG6$@qw6uRN0VG_YizA#_Ob6>-U>Enelf9D?3vIK}O|ZQjeEg+2p%;Jd q;8a+seDrV==VURJPn;BVS*~xC+u@k(3jS>jhS}S& z!?KYWn|v@ZXV1)kXU@)923xOe_tt@KK1Ru_K}41t@GNtTYr<@&bsH za8Ze7qF{0Mki*XpbB3|e)tuYfYoERMx$v)JDC^(BD(2Bd2c!6jUjg7*Q>@TKZt z1MjgC^SAH~H^aCWj-!2rMLez&e#DMjn2ouI7%32k`(Zm8T3|RMbkLAe^(f|^hCvCI zFc})1Izk&W(I2X`CFpWzLhFLSZ|vhhimY4crt^_rhVDTa!?+ZQW=8mq=lB@&C)oy0 z3mgoGlLuIc^0(PV)WeYD{ar>`ZgD2AId((@!hr4}>x1^oF#wW~O zMLxnrAL0#c<7JHJq)2Y(r0R#WiMLV#PijO@v76aW;d8b=fv>XXy6(&DhS{ibaEyDk c@%*>=1A9R|raB6-`2YX_07*qoM6N<$f=80b3jhEB literal 0 HcmV?d00001 diff --git a/apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png b/apps/desktop/src/assets/images/tray/closed-windowsTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..30f5b476bd11ec2330b90ca671b6483f457a4b3e GIT binary patch literal 492 zcmV1no zgLw9%e^OR)gcUr&0MDz8zhkRKkfv-!j0r1=!O-+Acknv;*RU%|UqfDmFX4Mwo=mr3 zTr#+d8_~ann=OLFb)1X-Fk)1aVWVZBCTv8E326r47Cyz?Q7q_1t5sh?Prh}(!uQN% znJhKvh&1jo0o(H8?8kEvt?;$2!4~YxQ{5)rRTRGV(~0ymq}`|1lo{Xi1awIzFUG$n z;Jjqpmo4A}-qaan*j$Q~VA&a1ih#8_V*}Ikpi=>B_=3Bou~odoQ#HSU1&gSG;zo5F z7qEwKSZ}g;QAKzMKk*eGaiz+zt0LURAui)BJ~tIGUVx~xrJ&F7!10=>CByyb|FK}^ zFu;5Hy{|}nQ4^zi3)0i@2QHSxv=Oi;zjzdBe`%W4Ug|e}%T4*U{IThEyzW>&#H(a% iCdf%>^T*_r|C`^pV{U1Mlf)eW0000 !openedWindowIds.includes(win.windowId)) + .sort((a, b) => { + // If closedAt is null, it indicates an abnormal closure and should be placed at the end + if (a.closedAt === null && b.closedAt === null) return 0; + if (a.closedAt === null) return 1; + if (b.closedAt === null) return -1; + // Otherwise, sort by time in descending order + return b.closedAt - a.closedAt; + }); + const menuItems: Electron.MenuItemConstructorOptions[] = []; + for (const win of closedWindows) { + const activeCtx = win.contexts.find(c => c.active === true); + const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath; + const activateNoteId = activateNotePath?.split("/").pop() ?? null; + + // Get the title of the closed window + const rawTitle = activateNoteId ? becca_service.getNoteTitle(activateNoteId) : ""; + let winTitle = rawTitle.length > 20 ? `${rawTitle.slice(0, 17)}...` : rawTitle; + const mainTabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length; + if (mainTabCount > 1) { + const tabSuffix = t("tray.tabs-total", { number: mainTabCount }); + winTitle += ` (${tabSuffix})`; + } + + menuItems.push({ + label: winTitle, + type: "normal", + click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow() + }); + } + + return menuItems; + } + const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = []; // Only call getWindowTitle if windowVisibilityMap has more than one window @@ -258,6 +296,12 @@ function updateTrayMenu() { icon: getIconPath("recents"), submenu: buildRecentNotesMenu() }, + { + label: t("tray.recently-closed-windows"), + type: "submenu", + icon: getIconPath("closed-windows"), + submenu: buildClosedWindowsMenu() + }, { type: "separator" }, { label: t("tray.close"), From 36dd29f919d77a5dc1596941553703f231cd8ded Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 14:37:40 +0800 Subject: [PATCH 04/27] feat(window): add class to extra windows --- apps/server/src/assets/views/desktop.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/assets/views/desktop.ejs b/apps/server/src/assets/views/desktop.ejs index 8d53b914c..4a4b59436 100644 --- a/apps/server/src/assets/views/desktop.ejs +++ b/apps/server/src/assets/views/desktop.ejs @@ -12,7 +12,7 @@ From 3f0abce874156101cdff78b108e69798a0178227 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 14:43:49 +0800 Subject: [PATCH 05/27] feat(window_db): migrate openNoteContexts to structured format with window metadata --- ...0234__migrate_open_note_contexts_format.ts | 42 +++++++++++++++++++ apps/server/src/migrations/migrations.ts | 5 +++ apps/server/src/services/app_info.ts | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts diff --git a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts new file mode 100644 index 000000000..72b09ed2a --- /dev/null +++ b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts @@ -0,0 +1,42 @@ +import cls from "../services/cls.js"; +import sql from "../services/sql.js"; + +export default () => { + cls.init(() => { + const row = sql.getRow<{ value: string }>( + `SELECT value FROM options WHERE name = 'openNoteContexts'` + ); + + if (!row || !row.value) { + return; + } + + let parsed: any; + try { + parsed = JSON.parse(row.value); + } catch { + return; + } + + // Already in new format, skip + if (parsed[0].windowId) { + return; + } + + // Old format: just contexts + const migrated = [ + { + windowId: "main", + createdAt: 0, + closedAt: null, + contexts: parsed + } + ]; + + sql.execute( + `UPDATE options SET value = ? WHERE name = 'openNoteContexts'`, + [JSON.stringify(migrated)] + ); + + }); +}; diff --git a/apps/server/src/migrations/migrations.ts b/apps/server/src/migrations/migrations.ts index 2757b4c25..0f7f6e40d 100644 --- a/apps/server/src/migrations/migrations.ts +++ b/apps/server/src/migrations/migrations.ts @@ -6,6 +6,11 @@ // Migrations should be kept in descending order, so the latest migration is first. const MIGRATIONS: (SqlMigration | JsMigration)[] = [ + // Migrate openNoteContexts option to the new structured format with window metadata + { + version: 234, + module: async () => import("./0234__migrate_open_note_contexts_format") + }, // Migrate geo map to collection { version: 233, diff --git a/apps/server/src/services/app_info.ts b/apps/server/src/services/app_info.ts index 2837e8de7..002f9c43b 100644 --- a/apps/server/src/services/app_info.ts +++ b/apps/server/src/services/app_info.ts @@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" }; import dataDir from "./data_dir.js"; import { AppInfo } from "@triliumnext/commons"; -const APP_DB_VERSION = 233; +const APP_DB_VERSION = 234; const SYNC_VERSION = 36; const CLIPPER_PROTOCOL_VERSION = "1.0"; From d0a22bc517c7cbfc2385ef67f7cb08d8a914ec69 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 15:27:11 +0800 Subject: [PATCH 06/27] fix(window): Fix empty array issue during openNoteContents data migration --- .../0234__migrate_open_note_contexts_format.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts index 72b09ed2a..c04bcc073 100644 --- a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts +++ b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts @@ -18,8 +18,13 @@ export default () => { return; } - // Already in new format, skip - if (parsed[0].windowId) { + // Already in new format (array + windowId), skip + if ( + Array.isArray(parsed) && + parsed.length > 0 && + typeof parsed[0] === "object" && + parsed[0].windowId + ) { return; } From 537c4051cc117e646960d9dae6591035390d2f11 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 15:27:35 +0800 Subject: [PATCH 07/27] feat(window): add class to extra windows --- apps/server/src/assets/views/desktop.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/assets/views/desktop.ejs b/apps/server/src/assets/views/desktop.ejs index 452a2d1d5..a9bcb7535 100644 --- a/apps/server/src/assets/views/desktop.ejs +++ b/apps/server/src/assets/views/desktop.ejs @@ -15,7 +15,7 @@ From 5d39b84886a2e3c77dc84525602722f87c0f2b28 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 15:28:27 +0800 Subject: [PATCH 08/27] fix(window): Fix incorrect noteContents error --- apps/client/src/components/tab_manager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index ee00cf0cb..6d48a80bb 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -59,7 +59,7 @@ export default class TabManager extends Component { windowId: appContext.windowId, createdAt: Date.now(), closedAt: null, - contexts: savedWindows + contexts: openNoteContexts }); } @@ -135,17 +135,17 @@ export default class TabManager extends Component { // Save window contents if (currentWin) { - currentWin.createdAt = Date.now(), + currentWin.createdAt = Date.now(); currentWin.closedAt = null; currentWin.contexts = filteredNoteContexts; } else { // Filter out the oldest entry (excluding the main window) - if (savedWindows.length >= 10) { + if (savedWindows?.length >= 10) { const candidates = savedWindows.filter(w => w.windowId !== "main"); - const oldestIndex = candidates.reduce((a, b) => + const oldest = candidates.reduce((a, b) => a.createdAt < b.createdAt ? a : b ); - savedWindows.splice(savedWindows.indexOf(oldestIndex), 1); + savedWindows.splice(savedWindows.indexOf(oldest), 1); } savedWindows.push({ windowId: appContext.windowId, From 0af7b8b14516bef2880420b76d728883db17cf07 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 15:56:04 +0800 Subject: [PATCH 09/27] chore(window): use MAX_SAVED_WINDOWS constant --- apps/client/src/components/tab_manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 6d48a80bb..b57d23983 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -11,6 +11,8 @@ import linkService from "../services/link.js"; import type { EventData } from "./app_context.js"; import type FNote from "../entities/fnote.js"; +const MAX_SAVED_WINDOWS = 10; + interface TabState { contexts: NoteContext[]; position: number; @@ -140,7 +142,7 @@ export default class TabManager extends Component { currentWin.contexts = filteredNoteContexts; } else { // Filter out the oldest entry (excluding the main window) - if (savedWindows?.length >= 10) { + if (savedWindows?.length >= MAX_SAVED_WINDOWS) { const candidates = savedWindows.filter(w => w.windowId !== "main"); const oldest = candidates.reduce((a, b) => a.createdAt < b.createdAt ? a : b From 2e30683b7b519348fbd903aac9f4021466c6764b Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 16:08:29 +0800 Subject: [PATCH 10/27] chore(window): avoid reduce error when no candidates --- apps/client/src/components/tab_manager.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index b57d23983..682cf29ec 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -144,10 +144,12 @@ export default class TabManager extends Component { // Filter out the oldest entry (excluding the main window) if (savedWindows?.length >= MAX_SAVED_WINDOWS) { const candidates = savedWindows.filter(w => w.windowId !== "main"); - const oldest = candidates.reduce((a, b) => - a.createdAt < b.createdAt ? a : b - ); - savedWindows.splice(savedWindows.indexOf(oldest), 1); + if (candidates.length > 0) { + const oldest = candidates.reduce((a, b) => + a.createdAt < b.createdAt ? a : b + ); + savedWindows.splice(savedWindows.indexOf(oldest), 1); + } } savedWindows.push({ windowId: appContext.windowId, From c28f11336e3169a584f070d37d1d7952ca95371f Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 16:11:49 +0800 Subject: [PATCH 11/27] chore(window_db): fix potential migration error --- .../src/migrations/0234__migrate_open_note_contexts_format.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts index c04bcc073..fd51a00cd 100644 --- a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts +++ b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts @@ -22,6 +22,7 @@ export default () => { if ( Array.isArray(parsed) && parsed.length > 0 && + parsed[0] && typeof parsed[0] === "object" && parsed[0].windowId ) { From 254145f0e5517dd3e6532c2febe428d83cb4e578 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 16:26:50 +0800 Subject: [PATCH 12/27] chore(window): handle potential JSON parsing failures --- apps/server/src/services/options.ts | 14 ++++++++++++++ apps/server/src/services/tray.ts | 2 +- apps/server/src/services/window.ts | 4 +--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/options.ts b/apps/server/src/services/options.ts index 1cc67df5a..aaf758c87 100644 --- a/apps/server/src/services/options.ts +++ b/apps/server/src/services/options.ts @@ -72,6 +72,19 @@ function getOptionBool(name: FilterOptionsByType): boolean { return val === "true"; } +function getOptionJson(name: OptionNames) { + const val = getOptionOrNull(name); + + if (typeof val !== "string") { + return null; + } + try { + return JSON.parse(val); + } catch (e) { + return null; + } +} + function setOption(name: T, value: string | OptionDefinitions[T]) { const option = becca.getOption(name); @@ -137,6 +150,7 @@ export default { getOption, getOptionInt, getOptionBool, + getOptionJson, setOption, createOption, getOptions, diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 008635794..739f522f9 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -197,7 +197,7 @@ function updateTrayMenu() { } function buildClosedWindowsMenu() { - const savedOpenNoteContexts = JSON.parse(optionService.getOption("openNoteContexts") || "[]"); + const savedOpenNoteContexts = optionService.getOptionJson("openNoteContexts") || "[]"; const openedWindowIds = windowService.getAllWindowIds(); const closedWindows = savedOpenNoteContexts .filter(win => !openedWindowIds.includes(win.windowId)) diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index ceaca56b6..1a1d055f0 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -37,9 +37,7 @@ function trackWindowFocus(win: BrowserWindow, windowId: string) { win.on("closed", () => { cls.wrap(() => { - const savedWindows = JSON.parse( - optionService.getOption("openNoteContexts") || "[]" - ); + const savedWindows = optionService.getOptionJson("openNoteContexts") || "[]"; const win = savedWindows.find(w => w.windowId === windowId); if (win) { From 07871853a5fd5946397c20b0b80652f18c3b7cb8 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 29 Dec 2025 19:24:53 +0800 Subject: [PATCH 13/27] fix(window): cannot save when switching between multiple windows --- apps/client/src/services/froca_updater.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 6d6ef9213..9f7c8a3ed 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -27,10 +27,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) { loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId); } else if (ec.entityName === "options") { const attributeEntity = ec.entity as FAttributeRow; - if (attributeEntity.name === "openNoteContexts") { - continue; // only noise - } - options.set(attributeEntity.name as OptionNames, attributeEntity.value); loadResults.addOption(attributeEntity.name as OptionNames); } else if (ec.entityName === "attachments") { From 366166a561bb9acf2c58e8f05a3f38588246cba7 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 30 Dec 2025 09:44:58 +0800 Subject: [PATCH 14/27] fix(window): avoid invalid fallback value for openNoteContexts --- apps/client/src/components/tab_manager.ts | 4 ++-- apps/server/src/services/tray.ts | 4 ++-- apps/server/src/services/window.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 682cf29ec..d0fa40730 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -52,7 +52,7 @@ export default class TabManager extends Component { .filter((t) => !!t); // Update the current window’s openNoteContexts in options - const savedWindows = options.getJson("openNoteContexts"); + const savedWindows = options.getJson("openNoteContexts") || []; const win = savedWindows.find(w => w.windowId === appContext.windowId); if (win) { win.contexts = openNoteContexts; @@ -81,7 +81,7 @@ export default class TabManager extends Component { async loadTabs() { // Get the current window’s openNoteContexts - const savedWindows = options.getJson("openNoteContexts"); + const savedWindows = options.getJson("openNoteContexts") || []; const currentWin = savedWindows.find(w => w.windowId === appContext.windowId); const openNoteContexts = currentWin ? currentWin.contexts : undefined; diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 739f522f9..72b90cd42 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -197,9 +197,9 @@ function updateTrayMenu() { } function buildClosedWindowsMenu() { - const savedOpenNoteContexts = optionService.getOptionJson("openNoteContexts") || "[]"; + const savedWindows = optionService.getOptionJson("openNoteContexts") || []; const openedWindowIds = windowService.getAllWindowIds(); - const closedWindows = savedOpenNoteContexts + const closedWindows = savedWindows .filter(win => !openedWindowIds.includes(win.windowId)) .sort((a, b) => { // If closedAt is null, it indicates an abnormal closure and should be placed at the end diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index 1a1d055f0..4796f1a62 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -37,7 +37,7 @@ function trackWindowFocus(win: BrowserWindow, windowId: string) { win.on("closed", () => { cls.wrap(() => { - const savedWindows = optionService.getOptionJson("openNoteContexts") || "[]"; + const savedWindows = optionService.getOptionJson("openNoteContexts") || []; const win = savedWindows.find(w => w.windowId === windowId); if (win) { From 10781077762265f048b1c912b2c2e730670fd65b Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Thu, 1 Jan 2026 14:30:01 +0800 Subject: [PATCH 15/27] chore(window): initialize closed time of openNoteContents to 0 --- apps/client/src/components/tab_manager.ts | 4 ++-- .../0234__migrate_open_note_contexts_format.ts | 2 +- apps/server/src/services/options_init.ts | 4 ++-- apps/server/src/services/sql_init.ts | 2 +- apps/server/src/services/tray.ts | 10 ++-------- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index d0fa40730..47305b2a5 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -60,7 +60,7 @@ export default class TabManager extends Component { savedWindows.push({ windowId: appContext.windowId, createdAt: Date.now(), - closedAt: null, + closedAt: 0, contexts: openNoteContexts }); } @@ -154,7 +154,7 @@ export default class TabManager extends Component { savedWindows.push({ windowId: appContext.windowId, createdAt: Date.now(), - closedAt: null, + closedAt: 0, contexts: filteredNoteContexts }); } diff --git a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts index fd51a00cd..15dba5d6f 100644 --- a/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts +++ b/apps/server/src/migrations/0234__migrate_open_note_contexts_format.ts @@ -34,7 +34,7 @@ export default () => { { windowId: "main", createdAt: 0, - closedAt: null, + closedAt: 0, contexts: parsed } ]; diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index a05fee0b0..eb7219ece 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -47,7 +47,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = { windowId: "main", createdAt: 0, - closedAt: null, + closedAt: 0, contexts: [ { notePath: "root", @@ -266,7 +266,7 @@ function initStartupOptions() { { windowId: "main", createdAt: 0, - closedAt: null, + closedAt: 0, contexts: [ { notePath: process.env.TRILIUM_START_NOTE_ID || "root", diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index dc1adc11c..f203882fa 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -149,7 +149,7 @@ async function createInitialDatabase(skipDemoDb?: boolean) { { windowId: "main", createdAt: 0, - closedAt: null, + closedAt: 0, contexts: [ { notePath: startNoteId, diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 72b90cd42..151479ccb 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -201,15 +201,9 @@ function updateTrayMenu() { const openedWindowIds = windowService.getAllWindowIds(); const closedWindows = savedWindows .filter(win => !openedWindowIds.includes(win.windowId)) - .sort((a, b) => { - // If closedAt is null, it indicates an abnormal closure and should be placed at the end - if (a.closedAt === null && b.closedAt === null) return 0; - if (a.closedAt === null) return 1; - if (b.closedAt === null) return -1; - // Otherwise, sort by time in descending order - return b.closedAt - a.closedAt; - }); + .sort((a, b) => { return b.closedAt - a.closedAt; }); // sort by time in descending order const menuItems: Electron.MenuItemConstructorOptions[] = []; + for (const win of closedWindows) { const activeCtx = win.contexts.find(c => c.active === true); const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath; From 79e2c97882a4aafb27ea76e754de34c1cdc5c7e1 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Thu, 1 Jan 2026 16:09:03 +0800 Subject: [PATCH 16/27] chore(window): initialize closed time of openNoteContents to 0 --- apps/client/src/components/tab_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 47305b2a5..6b0a48b08 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -138,7 +138,7 @@ export default class TabManager extends Component { // Save window contents if (currentWin) { currentWin.createdAt = Date.now(); - currentWin.closedAt = null; + currentWin.closedAt = 0; currentWin.contexts = filteredNoteContexts; } else { // Filter out the oldest entry (excluding the main window) From 27dc66263657378ae1fad17eea4d934ff266a167 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Fri, 2 Jan 2026 18:17:05 +0800 Subject: [PATCH 17/27] fix(window): a window with no open notes appears blank. --- apps/server/src/services/tray.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 151479ccb..8f373a293 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -208,15 +208,15 @@ function updateTrayMenu() { const activeCtx = win.contexts.find(c => c.active === true); const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath; const activateNoteId = activateNotePath?.split("/").pop() ?? null; - + if (!activateNoteId) continue; + // Get the title of the closed window - const rawTitle = activateNoteId ? becca_service.getNoteTitle(activateNoteId) : ""; - let winTitle = rawTitle.length > 20 ? `${rawTitle.slice(0, 17)}...` : rawTitle; - const mainTabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length; - if (mainTabCount > 1) { - const tabSuffix = t("tray.tabs-total", { number: mainTabCount }); - winTitle += ` (${tabSuffix})`; - } + const winTitle = (() => { + const raw = becca_service.getNoteTitle(activateNoteId); + const truncated = raw.length > 20 ? `${raw.slice(0, 17)}…` : raw; + const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length; + return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated; + })(); menuItems.push({ label: winTitle, From 2fd5ddab8654bee79f23b343be5b6e5a65e37e32 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sat, 3 Jan 2026 11:07:48 +0800 Subject: [PATCH 18/27] chore(window): optimize the replacement logic for old window notes --- apps/client/src/components/tab_manager.ts | 33 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 6b0a48b08..04b66afc4 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -141,16 +141,35 @@ export default class TabManager extends Component { currentWin.closedAt = 0; currentWin.contexts = filteredNoteContexts; } else { - // Filter out the oldest entry (excluding the main window) if (savedWindows?.length >= MAX_SAVED_WINDOWS) { - const candidates = savedWindows.filter(w => w.windowId !== "main"); - if (candidates.length > 0) { - const oldest = candidates.reduce((a, b) => - a.createdAt < b.createdAt ? a : b - ); - savedWindows.splice(savedWindows.indexOf(oldest), 1); + // Filter out the oldest entry + // 1) Never remove the "main" window + // 2) Prefer removing the oldest closed window (closedAt !== 0) + // 3) If no closed window exists, remove the window with the oldest created window + let oldestClosedIndex = -1; + let oldestClosedTime = Infinity; + let oldestCreatedIndex = -1; + let oldestCreatedTime = Infinity; + savedWindows.forEach((w, i) => { + if (w.windowId === "main") return; + if (w.closedAt !== 0) { + if (w.closedAt < oldestClosedTime) { + oldestClosedTime = w.closedAt; + oldestClosedIndex = i; + } + } else { + if (w.createdAt < oldestCreatedTime) { + oldestCreatedTime = w.createdAt; + oldestCreatedIndex = i; + } + } + }); + const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex; + if (indexToRemove !== -1) { + savedWindows.splice(indexToRemove, 1); } } + savedWindows.push({ windowId: appContext.windowId, createdAt: Date.now(), From 1bb206d97850be001e16a019c67aef4051635bfc Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sat, 3 Jan 2026 18:15:45 +0800 Subject: [PATCH 19/27] chore(i18n): tabs total --- apps/server/src/assets/translations/en/server.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/assets/translations/en/server.json b/apps/server/src/assets/translations/en/server.json index 2070c895e..3f307d359 100644 --- a/apps/server/src/assets/translations/en/server.json +++ b/apps/server/src/assets/translations/en/server.json @@ -383,7 +383,7 @@ "close": "Quit Trilium", "recents": "Recent notes", "recently-closed-windows": "Recently closed windows", - "tabs-total": "total {{number}} tabs", + "tabs-total": "{{number}} tabs total", "bookmarks": "Bookmarks", "today": "Open today's journal note", "new-note": "New note", From de1ef5b98b260e518593807fe59789b0256098a1 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sat, 3 Jan 2026 19:04:34 +0800 Subject: [PATCH 20/27] chore(test): fix errors caused by layout changes --- apps/server-e2e/src/note_types/text.spec.ts | 2 +- apps/server-e2e/src/support/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server-e2e/src/note_types/text.spec.ts b/apps/server-e2e/src/note_types/text.spec.ts index c99ad5d7d..89b186989 100644 --- a/apps/server-e2e/src/note_types/text.spec.ts +++ b/apps/server-e2e/src/note_types/text.spec.ts @@ -42,7 +42,7 @@ test("Highlights list is displayed", async ({ page, context }) => { await app.closeAllTabs(); await app.goToNoteInNewTab("Highlights list"); - await expect(app.sidebar).toContainText("Highlights List"); + await expect(app.sidebar).toContainText(/highlights/i); const rootList = app.sidebar.locator(".highlights-list ol"); let index = 0; for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) { diff --git a/apps/server-e2e/src/support/app.ts b/apps/server-e2e/src/support/app.ts index 12097c656..3df83db35 100644 --- a/apps/server-e2e/src/support/app.ts +++ b/apps/server-e2e/src/support/app.ts @@ -59,7 +59,7 @@ export default class App { // Wait for the page to load. if (url === "/") { - await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test"); + await expect(this.noteTree).toContainText("Trilium Integration Test"); if (!preserveTabs) { await this.closeAllTabs(); } From 2821b6da9db6a20336f3e6806c432c91a13f01c5 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sat, 3 Jan 2026 20:04:17 +0800 Subject: [PATCH 21/27] chore(window): add TS type WindowState --- apps/client/src/components/tab_manager.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 04b66afc4..fdd918722 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -27,6 +27,13 @@ interface NoteContextState { viewScope: Record; } +interface WindowState { + windowId: string; + createdAt: number; + closedAt: number; + contexts: NoteContextState[]; +} + export default class TabManager extends Component { public children: NoteContext[]; public mutex: Mutex; @@ -53,7 +60,7 @@ export default class TabManager extends Component { // Update the current window’s openNoteContexts in options const savedWindows = options.getJson("openNoteContexts") || []; - const win = savedWindows.find(w => w.windowId === appContext.windowId); + const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId); if (win) { win.contexts = openNoteContexts; } else { @@ -62,7 +69,7 @@ export default class TabManager extends Component { createdAt: Date.now(), closedAt: 0, contexts: openNoteContexts - }); + } as WindowState); } await options.save("openNoteContexts", JSON.stringify(savedWindows)); @@ -136,7 +143,7 @@ export default class TabManager extends Component { }); // Save window contents - if (currentWin) { + if (currentWin as WindowState) { currentWin.createdAt = Date.now(); currentWin.closedAt = 0; currentWin.contexts = filteredNoteContexts; @@ -150,7 +157,7 @@ export default class TabManager extends Component { let oldestClosedTime = Infinity; let oldestCreatedIndex = -1; let oldestCreatedTime = Infinity; - savedWindows.forEach((w, i) => { + savedWindows.forEach((w: WindowState, i: number) => { if (w.windowId === "main") return; if (w.closedAt !== 0) { if (w.closedAt < oldestClosedTime) { @@ -175,7 +182,7 @@ export default class TabManager extends Component { createdAt: Date.now(), closedAt: 0, contexts: filteredNoteContexts - }); + } as WindowState); } await options.save("openNoteContexts", JSON.stringify(savedWindows)); From 07f3c48d0b6b6d4eb49122db6584ad2f887562de Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sun, 4 Jan 2026 14:13:18 +0800 Subject: [PATCH 22/27] chore(window): import randomString only when needed --- apps/desktop/src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index ecbbd2024..8db9f91da 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,7 +6,6 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js"; import windowService from "@triliumnext/server/src/services/window.js"; import tray from "@triliumnext/server/src/services/tray.js"; import options from "@triliumnext/server/src/services/options.js"; -import { randomString } from "@triliumnext/server/src/services/utils.js"; import electronDebug from "electron-debug"; import electronDl from "electron-dl"; @@ -71,9 +70,10 @@ async function main() { globalShortcut.unregisterAll(); }); - app.on("second-instance", (event, commandLine) => { + app.on("second-instance", async (event, commandLine) => { const lastFocusedWindow = windowService.getLastFocusedWindow(); if (commandLine.includes("--new-window")) { + const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString; const extraWindowId = randomString(4); windowService.createExtraWindow(extraWindowId, ""); } else if (lastFocusedWindow) { From 9e8d89a170f5973a8537636c8da3bbf676486439 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sun, 4 Jan 2026 15:18:57 +0800 Subject: [PATCH 23/27] chore(window): avoid missing windowId --- apps/client/src/test/setup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/test/setup.ts b/apps/client/src/test/setup.ts index f15c07567..ccaf91d88 100644 --- a/apps/client/src/test/setup.ts +++ b/apps/client/src/test/setup.ts @@ -13,7 +13,8 @@ function injectGlobals() { uncheckedWindow.$ = $; uncheckedWindow.WebSocket = () => {}; uncheckedWindow.glob = { - isMainWindow: true + isMainWindow: true, + windowId: "main" }; } From 3a9e6865330e79a11a71b3cee00576ded6dd306f Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sun, 4 Jan 2026 18:49:08 +0800 Subject: [PATCH 24/27] chore(window): simplify replacement logic for open windows --- apps/client/src/components/tab_manager.ts | 33 +++++++---------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index fdd918722..8799a80c2 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -149,31 +149,18 @@ export default class TabManager extends Component { currentWin.contexts = filteredNoteContexts; } else { if (savedWindows?.length >= MAX_SAVED_WINDOWS) { - // Filter out the oldest entry + // Filter out a window to remove // 1) Never remove the "main" window - // 2) Prefer removing the oldest closed window (closedAt !== 0) - // 3) If no closed window exists, remove the window with the oldest created window - let oldestClosedIndex = -1; - let oldestClosedTime = Infinity; - let oldestCreatedIndex = -1; - let oldestCreatedTime = Infinity; - savedWindows.forEach((w: WindowState, i: number) => { - if (w.windowId === "main") return; - if (w.closedAt !== 0) { - if (w.closedAt < oldestClosedTime) { - oldestClosedTime = w.closedAt; - oldestClosedIndex = i; - } - } else { - if (w.createdAt < oldestCreatedTime) { - oldestCreatedTime = w.createdAt; - oldestCreatedIndex = i; - } + // 2) Remove the window with the oldest creation time + let windowToRemove: WindowState | null = null; + for (const win of savedWindows) { + if (win.windowId === "main") continue; // never remove main + if (!windowToRemove || win.createdAt < windowToRemove.createdAt) { + windowToRemove = win; } - }); - const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex; - if (indexToRemove !== -1) { - savedWindows.splice(indexToRemove, 1); + } + if (windowToRemove) { + savedWindows.splice(savedWindows.indexOf(windowToRemove), 1); } } From c612bdbfc16fdde61f09705b0017420090db82ac Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 5 Jan 2026 11:21:40 +0800 Subject: [PATCH 25/27] fix(window): normalize closedAt of OpenNoteContexts for abnormally closed windows --- apps/client/src/components/tab_manager.ts | 33 ++++++++++++++++------- apps/desktop/src/main.ts | 27 ++++++++++++++++++- apps/server/src/services/tray.ts | 5 ++-- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 8799a80c2..fdd918722 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -149,18 +149,31 @@ export default class TabManager extends Component { currentWin.contexts = filteredNoteContexts; } else { if (savedWindows?.length >= MAX_SAVED_WINDOWS) { - // Filter out a window to remove + // Filter out the oldest entry // 1) Never remove the "main" window - // 2) Remove the window with the oldest creation time - let windowToRemove: WindowState | null = null; - for (const win of savedWindows) { - if (win.windowId === "main") continue; // never remove main - if (!windowToRemove || win.createdAt < windowToRemove.createdAt) { - windowToRemove = win; + // 2) Prefer removing the oldest closed window (closedAt !== 0) + // 3) If no closed window exists, remove the window with the oldest created window + let oldestClosedIndex = -1; + let oldestClosedTime = Infinity; + let oldestCreatedIndex = -1; + let oldestCreatedTime = Infinity; + savedWindows.forEach((w: WindowState, i: number) => { + if (w.windowId === "main") return; + if (w.closedAt !== 0) { + if (w.closedAt < oldestClosedTime) { + oldestClosedTime = w.closedAt; + oldestClosedIndex = i; + } + } else { + if (w.createdAt < oldestCreatedTime) { + oldestCreatedTime = w.createdAt; + oldestCreatedIndex = i; + } } - } - if (windowToRemove) { - savedWindows.splice(savedWindows.indexOf(windowToRemove), 1); + }); + const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex; + if (indexToRemove !== -1) { + savedWindows.splice(indexToRemove, 1); } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 8db9f91da..b96046209 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -127,7 +127,8 @@ async function onReady() { } }); } - + + await normalizeOpenNoteContexts(); tray.createTray(); } else { await windowService.createSetupWindow(); @@ -136,6 +137,30 @@ async function onReady() { await windowService.registerGlobalShortcuts(); } +/** + * Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts. + * This function normalizes those timestamps to the current time for correct sorting/filtering. + */ +async function normalizeOpenNoteContexts() { + const savedWindows = options.getOptionJson("openNoteContexts") || []; + const now = Date.now(); + + let changed = false; + for (const win of savedWindows) { + if (win.windowId !== "main" && win.closedAt === 0) { + win.closedAt = now; + changed = true; + } + } + + if (changed) { + const { default: cls } = (await import("@triliumnext/server/src/services/cls.js")); + cls.wrap(() => { + options.setOption("openNoteContexts", JSON.stringify(savedWindows)); + })(); + } +} + function getElectronLocale() { const uiLocale = options.getOptionOrNull("locale"); const formattingLocale = options.getOptionOrNull("formattingLocale"); diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 8f373a293..3b0831aff 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -201,10 +201,11 @@ function updateTrayMenu() { const openedWindowIds = windowService.getAllWindowIds(); const closedWindows = savedWindows .filter(win => !openedWindowIds.includes(win.windowId)) - .sort((a, b) => { return b.closedAt - a.closedAt; }); // sort by time in descending order + .sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order const menuItems: Electron.MenuItemConstructorOptions[] = []; - for (const win of closedWindows) { + for (let i = closedWindows.length - 1; i >= 0; i--) { + const win = closedWindows[i]; const activeCtx = win.contexts.find(c => c.active === true); const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath; const activateNoteId = activateNotePath?.split("/").pop() ?? null; From 21cb8968499469cbf76291035bda61f95b1935cc Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 12 Jan 2026 09:58:43 +0800 Subject: [PATCH 26/27] feat(window): add the extra-window class to the body of extra windows --- apps/client/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/client/src/index.ts b/apps/client/src/index.ts index 795adc8cf..6cb618985 100644 --- a/apps/client/src/index.ts +++ b/apps/client/src/index.ts @@ -71,7 +71,7 @@ function loadIcons() { } function setBodyAttributes() { - const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob; + const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob; const classesToSet = [ device, `heading-style-${headingStyle}`, @@ -79,7 +79,8 @@ function setBodyAttributes() { `platform-${platform}`, isElectron && "electron", hasNativeTitleBar && "native-titlebar", - hasBackgroundEffects && "background-effects" + hasBackgroundEffects && "background-effects", + !isMainWindow && 'extra-window' ].filter(Boolean) as string[]; for (const classToSet of classesToSet) { From 91237918d894ea94f44241363b86036a9caa8ba0 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Wed, 14 Jan 2026 14:46:43 +0800 Subject: [PATCH 27/27] fix(window): windowId is always main on mobile devices --- apps/server/src/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index ffe1b05e6..52a46c16a 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -50,7 +50,7 @@ export function bootstrap(req: Request, res: Response) { appCssNoteIds: getAppCssNoteIds(), isDev, isMainWindow: view === "mobile" ? true : !req.query.extraWindow, - windowId: view !== "mobile" && req.query.extraWindow ? req.query.extraWindow : "main", + windowId: req.query.extraWindow ?? "main", isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(), triliumVersion: packageJson.version, assetPath,