diff --git a/src/client/EditorHost.tsx b/src/client/EditorHost.tsx
index 31a019e..2ae2124 100644
--- a/src/client/EditorHost.tsx
+++ b/src/client/EditorHost.tsx
@@ -113,6 +113,18 @@ export function EditorHost(props: {
// with the same path/scope — the only reload entry besides open/close.
const [reloadSeq, setReloadSeq] = useState(0)
+ // Manual refresh (issue #167 + PR #228): a dirty draft is dropped by the
+ // reload (the editor instance remounts), so confirm before discarding it.
+ const refreshFile = (): void => {
+ if (toolbar?.dirty === true) {
+ const confirmed = typeof window.confirm === 'function'
+ ? window.confirm(t('refreshUnsavedConfirm'))
+ : false
+ if (!confirmed) return
+ }
+ setReloadSeq(sequence => sequence + 1)
+ }
+
// Reactive prefs read: flipping editorExplorer re-renders this tab with no
// reload. The snapshot is the bare boolean so unrelated store churn never
// re-renders the editor.
@@ -426,7 +438,7 @@ export function EditorHost(props: {
className={css.iconButton}
aria-label={t('refresh')}
title={t('refresh')}
- onClick={() => { setReloadSeq(sequence => sequence + 1) }}
+ onClick={refreshFile}
>
diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts
index f1bdc62..fe0b11f 100644
--- a/src/client/locales-ar.ts
+++ b/src/client/locales-ar.ts
@@ -88,6 +88,8 @@ export const ar: Record = {
mermaidZoomReset: 'إعادة تعيين',
mermaidZoomHint: 'العجلة للتقريب · السحب للتحريك · Esc للإغلاق',
refresh: 'تحديث',
+ showInFolder: 'إظهار في المجلد',
+ refreshUnsavedConfirm: 'تغيّر الملف على القرص. سيؤدي التحديث إلى فقدان التعديلات غير المحفوظة. متابعة؟',
save: 'حفظ',
saved: 'تم الحفظ',
unsaved: 'غير محفوظ',
diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts
index cda2c46..748dc1d 100644
--- a/src/client/locales-de.ts
+++ b/src/client/locales-de.ts
@@ -73,6 +73,8 @@ export const de: Record = {
mermaidZoomReset: 'Zurücksetzen',
mermaidZoomHint: 'Scrollen zum Zoomen · Ziehen zum Verschieben · Esc zum Schließen',
refresh: 'Aktualisieren',
+ showInFolder: 'Im Ordner anzeigen',
+ refreshUnsavedConfirm: 'Die Datei wurde geändert. Beim Aktualisieren gehen ungespeicherte Änderungen verloren. Fortfahren?',
save: 'Speichern',
saved: 'Gespeichert',
unsaved: 'Nicht gespeichert',
diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts
index ebbd02e..1633cb2 100644
--- a/src/client/locales-fr.ts
+++ b/src/client/locales-fr.ts
@@ -80,6 +80,8 @@ export const fr: Record = {
mermaidZoomReset: 'Réinitialiser',
mermaidZoomHint: 'Molette pour zoomer · glisser pour déplacer · Échap pour fermer',
refresh: 'Actualiser',
+ showInFolder: 'Afficher dans le dossier',
+ refreshUnsavedConfirm: 'Le fichier a changé sur le disque. Actualiser supprimera les modifications non enregistrées. Continuer ?',
save: 'Enregistrer',
saved: 'Enregistré',
unsaved: 'Non enregistré',
diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts
index c2fa81c..b9b0739 100644
--- a/src/client/locales-hi.ts
+++ b/src/client/locales-hi.ts
@@ -87,6 +87,8 @@ export const hi: Record = {
mermaidZoomReset: 'रीसेट',
mermaidZoomHint: 'स्क्रॉल से ज़ूम · खींचें पैन करने · Esc से बंद',
refresh: 'ताज़ा करें',
+ showInFolder: 'फ़ोल्डर में दिखाएँ',
+ refreshUnsavedConfirm: 'डिस्क पर फ़ाइल बदल गई है। रीफ़्रेश करने पर असहेज किए गए संपादन खो जाएँगे। जारी रखें?',
save: 'सहेजें',
saved: 'सहेजा गया',
unsaved: 'असहेजित',
diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts
index 91ed6ad..cd34049 100644
--- a/src/client/locales-id.ts
+++ b/src/client/locales-id.ts
@@ -85,6 +85,8 @@ export const id: Record = {
mermaidZoomReset: 'Reset',
mermaidZoomHint: 'Gulir untuk zoom · seret untuk geser · Esc untuk tutup',
refresh: 'Segarkan',
+ showInFolder: 'Tampilkan di folder',
+ refreshUnsavedConfirm: 'File berubah di disk. Menyegarkan akan membuang edit yang belum disimpan. Lanjutkan?',
save: 'Simpan',
saved: 'Tersimpan',
unsaved: 'Belum disimpan',
diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts
index 9476777..841321f 100644
--- a/src/client/locales-it.ts
+++ b/src/client/locales-it.ts
@@ -78,6 +78,8 @@ export const it: Record = {
mermaidZoomReset: 'Ripristina',
mermaidZoomHint: 'Scorra per zoom · trascini per panoramica · Esc per chiudere',
refresh: 'Aggiorna',
+ showInFolder: 'Mostra nella cartella',
+ refreshUnsavedConfirm: 'Il file è cambiato su disco. Aggiornare scarterà le modifiche non salvate. Continuare?',
save: 'Salva',
saved: 'Salvato',
unsaved: 'Non salvato',
diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts
index c9d275b..3ba93c1 100644
--- a/src/client/locales-ja.ts
+++ b/src/client/locales-ja.ts
@@ -87,6 +87,8 @@ export const ja: Record = {
mermaidZoomReset: 'リセット',
mermaidZoomHint: 'スクロールで拡大 ・ ドラッグで移動 ・ Esc で閉じる',
refresh: '更新',
+ showInFolder: 'フォルダーで表示',
+ refreshUnsavedConfirm: 'ディスク上のファイルが変更されました。更新すると未保存の編集が失われます。続行しますか?',
save: '保存',
saved: '保存済み',
unsaved: '未保存',
diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts
index b22f2e7..3fbc2df 100644
--- a/src/client/locales-ko.ts
+++ b/src/client/locales-ko.ts
@@ -79,6 +79,8 @@ export const ko: Record = {
mermaidZoomReset: '초기화',
mermaidZoomHint: '휠로 확대/축소 · 드래그로 이동 · Esc로 닫기',
refresh: '새로고침',
+ showInFolder: '폴더에서 표시',
+ refreshUnsavedConfirm: '디스크의 파일이 변경되었습니다. 새로고침하면 저장되지 않은 편집 내용이 손실됩니다. 계속하시겠습니까?',
save: '저장',
saved: '저장됨',
unsaved: '저장 안 됨',
diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts
index 6b1d4f5..259bf31 100644
--- a/src/client/locales-nl.ts
+++ b/src/client/locales-nl.ts
@@ -85,6 +85,8 @@ export const nl: Record = {
mermaidZoomReset: 'Resetten',
mermaidZoomHint: 'Scrollen om te zoomen · slepen om te pannen · Esc om te sluiten',
refresh: 'Vernieuwen',
+ showInFolder: 'In map tonen',
+ refreshUnsavedConfirm: 'Het bestand is op schijf gewijzigd. Vernieuwen gooit niet-opgeslagen wijzigingen weg. Doorgaan?',
save: 'Opslaan',
saved: 'Opgeslagen',
unsaved: 'Niet opgeslagen',
diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts
index 47bce32..a6ef610 100644
--- a/src/client/locales-pl.ts
+++ b/src/client/locales-pl.ts
@@ -89,6 +89,8 @@ export const pl: Record = {
mermaidZoomReset: 'Resetuj',
mermaidZoomHint: 'Kółko: powiększenie · przeciąganie: panowanie · Esc: zamknięcie',
refresh: 'Odśwież',
+ showInFolder: 'Pokaż w folderze',
+ refreshUnsavedConfirm: 'Plik zmienił się na dysku. Odświeżenie odrzuci niezapisane zmiany. Kontynuować?',
save: 'Zapisz',
saved: 'Zapisano',
unsaved: 'Niezapisane',
diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts
index 3dd75ee..8c6c876 100644
--- a/src/client/locales-pt.ts
+++ b/src/client/locales-pt.ts
@@ -70,6 +70,8 @@ export const pt: Record = {
mermaidZoomReset: 'Redefinir',
mermaidZoomHint: 'Rolar para ampliar · arrastar para mover · Esc para fechar',
refresh: 'Atualizar',
+ showInFolder: 'Mostrar na pasta',
+ refreshUnsavedConfirm: 'O arquivo mudou no disco. Atualizar descartará edições não salvas. Continuar?',
save: 'Salvar',
saved: 'Salvo',
unsaved: 'Não salvo',
diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts
index 63db703..dab6236 100644
--- a/src/client/locales-ru.ts
+++ b/src/client/locales-ru.ts
@@ -83,6 +83,8 @@ export const ru: Record = {
mermaidZoomReset: 'Сбросить',
mermaidZoomHint: 'Колесо — масштаб · перетаскивание — панорама · Esc — закрыть',
refresh: 'Обновить',
+ showInFolder: 'Показать в папке',
+ refreshUnsavedConfirm: 'Файл изменился на диске. Обновление отбросит несохранённые изменения. Продолжить?',
save: 'Сохранить',
saved: 'Сохранено',
unsaved: 'Не сохранено',
diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts
index 3efe359..a6aed2f 100644
--- a/src/client/locales-sv.ts
+++ b/src/client/locales-sv.ts
@@ -70,6 +70,8 @@ export const sv: Record = {
mermaidZoomReset: 'Återställ',
mermaidZoomHint: 'Rulla för att zooma · dra för att panorera · Esc för att stänga',
refresh: 'Uppdatera',
+ showInFolder: 'Visa i mapp',
+ refreshUnsavedConfirm: 'Filen ändrades på disken. Uppdatering kastar osparade ändringar. Fortsätta?',
save: 'Spara',
saved: 'Sparad',
unsaved: 'Osparad',
diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts
index 63c1501..4431c93 100644
--- a/src/client/locales-th.ts
+++ b/src/client/locales-th.ts
@@ -87,6 +87,8 @@ export const th: Record = {
mermaidZoomReset: 'รีเซ็ต',
mermaidZoomHint: 'เลื่อนเพื่อซูม · ลากเพื่อแพน · Esc เพื่อปิด',
refresh: 'รีเฟรช',
+ showInFolder: 'แสดงในโฟลเดอร์',
+ refreshUnsavedConfirm: 'ไฟล์เปลี่ยนบนดิสก์ การรีเฟรชจะทิ้งการแก้ไขที่ยังไม่ได้บันทึก ดำเนินการต่อหรือไม่',
save: 'บันทึก',
saved: 'บันทึกแล้ว',
unsaved: 'ยังไม่ได้บันทึก',
diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts
index cb33f11..7e385d8 100644
--- a/src/client/locales-tr.ts
+++ b/src/client/locales-tr.ts
@@ -87,6 +87,8 @@ export const tr: Record = {
mermaidZoomReset: 'Sıfırla',
mermaidZoomHint: 'Yakınlaştırmak için kaydır · taşımak için sürükle · kapatmak için Esc',
refresh: 'Yenile',
+ showInFolder: 'Klasörde göster',
+ refreshUnsavedConfirm: 'Dosya diskte değişti. Yenileme kaydedilmemiş düzenlemeleri atar. Devam edilsin mi?',
save: 'Kaydet',
saved: 'Kaydedildi',
unsaved: 'Kaydedilmedi',
diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts
index 6cbac91..1578e52 100644
--- a/src/client/locales-vi.ts
+++ b/src/client/locales-vi.ts
@@ -87,6 +87,8 @@ export const vi: Record = {
mermaidZoomReset: 'Đặt lại',
mermaidZoomHint: 'Cuộn để zoom · kéo để di chuyển · Esc để đóng',
refresh: 'Làm mới',
+ showInFolder: 'Hiển thị trong thư mục',
+ refreshUnsavedConfirm: 'Tệp đã thay đổi trên đĩa. Làm mới sẽ loại bỏ các chỉnh sửa chưa lưu. Tiếp tục?',
save: 'Lưu',
saved: 'Đã lưu',
unsaved: 'Chưa lưu',
diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts
index 6be4a8b..9df284e 100644
--- a/src/client/locales-zh-HK.ts
+++ b/src/client/locales-zh-HK.ts
@@ -102,6 +102,8 @@ export const zhHK: Record = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
+ showInFolder: '在文件夾中顯示',
+ refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts
index 4d72472..ff6b5e7 100644
--- a/src/client/locales-zh-MO.ts
+++ b/src/client/locales-zh-MO.ts
@@ -102,6 +102,8 @@ export const zhMO: Record = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
+ showInFolder: '在文件夾中顯示',
+ refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts
index 2c153f2..05084e8 100644
--- a/src/client/locales-zh-TW.ts
+++ b/src/client/locales-zh-TW.ts
@@ -102,6 +102,8 @@ export const zhTW: Record = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
+ showInFolder: '在資料夾中顯示',
+ refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
diff --git a/src/index.ts b/src/index.ts
index d9e47ad..c7498da 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -533,8 +533,10 @@ function buildApi(
let response = await fetch(parsed, { method: 'HEAD', redirect: 'follow', signal: controller.signal })
// Some servers answer HEAD with 405/501; retry once as GET (the
// body is discarded — only the headers matter).
+ let retriedFromHeadRejection = false
if (response.status === 405 || response.status === 501) {
response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal })
+ retriedFromHeadRejection = true
}
// Some servers (e.g. aliyun consoles) answer HEAD without the
// X-Frame-Options / CSP headers that only their GET response
@@ -542,9 +544,11 @@ function buildApi(
// would wrongly report the site as embeddable and the plain iframe
// would surface the browser's misleading "refused to connect".
// Retry once as GET when both signals are absent (body discarded).
+ // A 405/501 retry already fetched the GET response, so the signals
+ // are either there or genuinely absent — another GET adds nothing.
const hasEmbedSignals = response.headers.get('content-security-policy') !== null
|| response.headers.get('x-frame-options') !== null
- if (!hasEmbedSignals && response.status !== 405 && response.status !== 501) {
+ if (!hasEmbedSignals && !retriedFromHeadRejection && response.status !== 405 && response.status !== 501) {
response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal })
}
const csp = response.headers.get('content-security-policy')
@@ -1006,13 +1010,6 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
agentListWss.close()
agentOpenWss.close()
}, 'dsh-better-sidebar: teardown')
-
- // Close all UI terminals of a session when the conversation is deleted —
- // terminals follow the session, not the reconnect grace. Agent terminals
- // are owned by the agent lifecycle and are not touched here.
- ctx.on('session/disposed', (session) => {
- try { ptyManager?.closeSession(session?.id) } catch {}
- }, { global: true })
}
/** Push queued `sidebar_open` requests for one session to a connected view. */
diff --git a/src/pty-manager.ts b/src/pty-manager.ts
index 93a56f5..0b0a068 100644
--- a/src/pty-manager.ts
+++ b/src/pty-manager.ts
@@ -243,12 +243,6 @@ export class PtyManager {
}
}
- /** Close every terminal of one session (the conversation was deleted —
- * terminals must follow the session, not linger until the grace expires). */
- closeSession(sessionId: string): void {
- for (const key of this.keysOf(sessionId)) this.close(key)
- }
-
/** Close every terminal (plugin teardown). */
disposeAll(): void {
for (const timer of this.pendingCloses.values()) clearTimeout(timer)
diff --git a/tests/builtins.spec.ts b/tests/builtins.spec.ts
index e1baa80..8eaac40 100644
--- a/tests/builtins.spec.ts
+++ b/tests/builtins.spec.ts
@@ -131,6 +131,7 @@ describe('built-in tab registrations', () => {
'browserInterceptLinks',
'browserInterceptHttp',
'browserInterceptHttps',
+ 'browserAllowedLoopback',
])
for (const toggle of toggles) {
expect(toggle.title).toBeDefined()
diff --git a/tests/editor-refresh.spec.tsx b/tests/editor-refresh.spec.tsx
index 387a060..a6e9734 100644
--- a/tests/editor-refresh.spec.tsx
+++ b/tests/editor-refresh.spec.tsx
@@ -80,6 +80,7 @@ function setup(initialMode: 'preview' | 'edit' = 'preview', dirty = false): {
const ctx = {
betterSidebar: service,
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
+ get: (name: string) => name === 'betterSidebar' ? service : undefined,
} as unknown as Context
ctx.betterSidebar.openTab({ type: 'editor', title: 'a.ts', path: '/tmp/a.ts', id: 'editor:/tmp/a.ts' })
const fileTab = (): SidebarTab =>
diff --git a/tests/free-window.spec.tsx b/tests/free-window.spec.tsx
index 568bfe2..963f720 100644
--- a/tests/free-window.spec.tsx
+++ b/tests/free-window.spec.tsx
@@ -99,6 +99,7 @@ function mountSidebar(sessionId: string = `s1-${++sessionSeq}`): Mounted {
locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot },
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
betterSidebar: service,
+ get: (name: string) => name === 'betterSidebar' ? service : undefined,
}
const root: Root = createRoot(container)
act(() => { root.render(createElement(Sidebar, { ctx: ctx as never, store })) })
diff --git a/tests/install-powershell.spec.ts b/tests/install-powershell.spec.ts
index bdcd029..6a7c3f5 100644
--- a/tests/install-powershell.spec.ts
+++ b/tests/install-powershell.spec.ts
@@ -79,7 +79,14 @@ describe('PowerShell installer entry points', () => {
const source = readFileSync(resolve(ROOT, file), 'utf8')
expect(source, file).not.toMatch(new RegExp(`irm [^\\r\\n]*${INSTALLER_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\r\\n]*\\|\\s*iex`, 'i'))
expect(source, file).not.toMatch(/\[scriptblock\]::Create\(\(irm\b/i)
- expect(source.match(/\.TrimStart\(\[char\]0xFEFF\)/g)?.length, file).toBeGreaterThanOrEqual(2)
+ // Where remote invocation IS documented, it must be BOM-safe: the
+ // READMEs switched to the prompt-based install flow and no longer
+ // document `irm` at all, so the TrimStart gate applies to the script
+ // (whose usage comment demonstrates the safe pattern).
+ const documentsInvocation = /\birm\b|\$script\s*=|\[scriptblock\]/.test(source)
+ if (documentsInvocation) {
+ expect(source.match(/\.TrimStart\(\[char\]0xFEFF\)/g)?.length, file).toBeGreaterThanOrEqual(2)
+ }
}
})
diff --git a/tests/markdown-manual-refresh.spec.tsx b/tests/markdown-manual-refresh.spec.tsx
index 6da925b..dd95b79 100644
--- a/tests/markdown-manual-refresh.spec.tsx
+++ b/tests/markdown-manual-refresh.spec.tsx
@@ -60,7 +60,10 @@ function setup(): {
.flatMap(leaf => leaf.tabs)
.find(candidate => candidate.type === 'editor')!
const tab: SidebarTab = { ...home, path: '/tmp/notes.md', title: 'notes.md' }
- const ctx = { betterSidebar: service } as unknown as Context
+ const ctx = {
+ betterSidebar: service,
+ get: (name: string) => name === 'betterSidebar' ? service : undefined,
+ } as unknown as Context
return { store, ctx, tab }
}
diff --git a/tests/plugin-shape.spec.ts b/tests/plugin-shape.spec.ts
index 992c573..81c1fb0 100644
--- a/tests/plugin-shape.spec.ts
+++ b/tests/plugin-shape.spec.ts
@@ -94,6 +94,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
const overridden = (PrefsSchema as unknown as {
(input: Record | undefined): Record
})({ openByDefault: false, defaultWidthPercent: 45 })
- expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
+ expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
})
})
diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts
index ad21a51..dd98d33 100644
--- a/tests/smoke.spec.ts
+++ b/tests/smoke.spec.ts
@@ -907,6 +907,7 @@ describe('side card settings routes', () => {
browserInterceptLinks: true,
browserInterceptHttp: true,
browserInterceptHttps: false,
+ browserAllowedLoopback: '',
// The enable-switch maps default to {} (everything on).
tabsEnabled: {},
viewersEnabled: {},
diff --git a/tests/state.spec.ts b/tests/state.spec.ts
index d542b12..4ca4ec3 100644
--- a/tests/state.spec.ts
+++ b/tests/state.spec.ts
@@ -871,7 +871,10 @@ describe('v0.12.0 store additions', () => {
// timers BOTH writes are pending and both land when they fire.
expect(timers.size).toBe(2)
for (const [, fn] of [...timers]) fn()
- expect(writes).toEqual(['dsh-sidebar:v1:a', 'dsh-sidebar:v1:b'])
+ // Each persist also syncs the shared cross-session width key (PR #36).
+ expect(writes.filter(key => key !== 'dsh-sidebar:v1:width'))
+ .toEqual(['dsh-sidebar:v1:a', 'dsh-sidebar:v1:b'])
+ expect(writes).toContain('dsh-sidebar:v1:width')
} finally {
delete g.window
delete g.localStorage