fix: 合并 main 后的测试回归修复

- index.ts: 移除 #130 遗留的 ctx.on('session/disposed')(fake ctx 无 on,
  且 #130 已按取 HEAD 决策,closeSession 调用方一并移除)
- browser.probe: 405→GET 重试后跳过 embed-signals GET,避免多余第 3 次请求(#69)
- EditorHost: 移植 #228 的 dirty 确认守卫到 HEAD 的 reloadSeq 结构(refreshFile)
- tests: fake ctx 补 get()(#357 ctx.get 回归,free-window/editor-refresh/markdown)
- tests: prefs 期望对象补 browserAllowedLoopback(smoke/plugin-shape/state)
- tests: builtins 浏览器设置行补 browserAllowedLoopback(#69)
- tests: install-powershell BOM 门限仅对实际文档化远程调用的文件生效
- locales: 19 语言补齐 showInFolder / refreshUnsavedConfirm(#94 #215 #228)
This commit is contained in:
HuanLinOTO
2026-08-24 17:54:33 +08:00
parent f4e71c1257
commit 6001f0167c
30 changed files with 77 additions and 19 deletions
+13 -1
View File
@@ -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}
>
<IconRefreshOutline14 size={14} />
</button>
+2
View File
@@ -88,6 +88,8 @@ export const ar: Record<string, string> = {
mermaidZoomReset: 'إعادة تعيين',
mermaidZoomHint: 'العجلة للتقريب · السحب للتحريك · Esc للإغلاق',
refresh: 'تحديث',
showInFolder: 'إظهار في المجلد',
refreshUnsavedConfirm: 'تغيّر الملف على القرص. سيؤدي التحديث إلى فقدان التعديلات غير المحفوظة. متابعة؟',
save: 'حفظ',
saved: 'تم الحفظ',
unsaved: 'غير محفوظ',
+2
View File
@@ -73,6 +73,8 @@ export const de: Record<string, string> = {
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',
+2
View File
@@ -80,6 +80,8 @@ export const fr: Record<string, string> = {
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é',
+2
View File
@@ -87,6 +87,8 @@ export const hi: Record<string, string> = {
mermaidZoomReset: 'रीसेट',
mermaidZoomHint: 'स्क्रॉल से ज़ूम · खींचें पैन करने · Esc से बंद',
refresh: 'ताज़ा करें',
showInFolder: 'फ़ोल्डर में दिखाएँ',
refreshUnsavedConfirm: 'डिस्क पर फ़ाइल बदल गई है। रीफ़्रेश करने पर असहेज किए गए संपादन खो जाएँगे। जारी रखें?',
save: 'सहेजें',
saved: 'सहेजा गया',
unsaved: 'असहेजित',
+2
View File
@@ -85,6 +85,8 @@ export const id: Record<string, string> = {
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',
+2
View File
@@ -78,6 +78,8 @@ export const it: Record<string, string> = {
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',
+2
View File
@@ -87,6 +87,8 @@ export const ja: Record<string, string> = {
mermaidZoomReset: 'リセット',
mermaidZoomHint: 'スクロールで拡大 ・ ドラッグで移動 ・ Esc で閉じる',
refresh: '更新',
showInFolder: 'フォルダーで表示',
refreshUnsavedConfirm: 'ディスク上のファイルが変更されました。更新すると未保存の編集が失われます。続行しますか?',
save: '保存',
saved: '保存済み',
unsaved: '未保存',
+2
View File
@@ -79,6 +79,8 @@ export const ko: Record<string, string> = {
mermaidZoomReset: '초기화',
mermaidZoomHint: '휠로 확대/축소 · 드래그로 이동 · Esc로 닫기',
refresh: '새로고침',
showInFolder: '폴더에서 표시',
refreshUnsavedConfirm: '디스크의 파일이 변경되었습니다. 새로고침하면 저장되지 않은 편집 내용이 손실됩니다. 계속하시겠습니까?',
save: '저장',
saved: '저장됨',
unsaved: '저장 안 됨',
+2
View File
@@ -85,6 +85,8 @@ export const nl: Record<string, string> = {
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',
+2
View File
@@ -89,6 +89,8 @@ export const pl: Record<string, string> = {
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',
+2
View File
@@ -70,6 +70,8 @@ export const pt: Record<string, string> = {
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',
+2
View File
@@ -83,6 +83,8 @@ export const ru: Record<string, string> = {
mermaidZoomReset: 'Сбросить',
mermaidZoomHint: 'Колесо — масштаб · перетаскивание — панорама · Esc — закрыть',
refresh: 'Обновить',
showInFolder: 'Показать в папке',
refreshUnsavedConfirm: 'Файл изменился на диске. Обновление отбросит несохранённые изменения. Продолжить?',
save: 'Сохранить',
saved: 'Сохранено',
unsaved: 'Не сохранено',
+2
View File
@@ -70,6 +70,8 @@ export const sv: Record<string, string> = {
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',
+2
View File
@@ -87,6 +87,8 @@ export const th: Record<string, string> = {
mermaidZoomReset: 'รีเซ็ต',
mermaidZoomHint: 'เลื่อนเพื่อซูม · ลากเพื่อแพน · Esc เพื่อปิด',
refresh: 'รีเฟรช',
showInFolder: 'แสดงในโฟลเดอร์',
refreshUnsavedConfirm: 'ไฟล์เปลี่ยนบนดิสก์ การรีเฟรชจะทิ้งการแก้ไขที่ยังไม่ได้บันทึก ดำเนินการต่อหรือไม่',
save: 'บันทึก',
saved: 'บันทึกแล้ว',
unsaved: 'ยังไม่ได้บันทึก',
+2
View File
@@ -87,6 +87,8 @@ export const tr: Record<string, string> = {
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',
+2
View File
@@ -87,6 +87,8 @@ export const vi: Record<string, string> = {
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',
+2
View File
@@ -102,6 +102,8 @@ export const zhHK: Record<string, string> = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
showInFolder: '在文件夾中顯示',
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
+2
View File
@@ -102,6 +102,8 @@ export const zhMO: Record<string, string> = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
showInFolder: '在文件夾中顯示',
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
+2
View File
@@ -102,6 +102,8 @@ export const zhTW: Record<string, string> = {
mermaidZoomReset: '重設',
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
refresh: '重新整理',
showInFolder: '在資料夾中顯示',
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
save: '儲存',
saved: '已儲存',
unsaved: '未儲存',
+5 -8
View File
@@ -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. */
-6
View File
@@ -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)