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:
@@ -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>
|
||||
|
||||
@@ -88,6 +88,8 @@ export const ar: Record<string, string> = {
|
||||
mermaidZoomReset: 'إعادة تعيين',
|
||||
mermaidZoomHint: 'العجلة للتقريب · السحب للتحريك · Esc للإغلاق',
|
||||
refresh: 'تحديث',
|
||||
showInFolder: 'إظهار في المجلد',
|
||||
refreshUnsavedConfirm: 'تغيّر الملف على القرص. سيؤدي التحديث إلى فقدان التعديلات غير المحفوظة. متابعة؟',
|
||||
save: 'حفظ',
|
||||
saved: 'تم الحفظ',
|
||||
unsaved: 'غير محفوظ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -87,6 +87,8 @@ export const hi: Record<string, string> = {
|
||||
mermaidZoomReset: 'रीसेट',
|
||||
mermaidZoomHint: 'स्क्रॉल से ज़ूम · खींचें पैन करने · Esc से बंद',
|
||||
refresh: 'ताज़ा करें',
|
||||
showInFolder: 'फ़ोल्डर में दिखाएँ',
|
||||
refreshUnsavedConfirm: 'डिस्क पर फ़ाइल बदल गई है। रीफ़्रेश करने पर असहेज किए गए संपादन खो जाएँगे। जारी रखें?',
|
||||
save: 'सहेजें',
|
||||
saved: 'सहेजा गया',
|
||||
unsaved: 'असहेजित',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -87,6 +87,8 @@ export const ja: Record<string, string> = {
|
||||
mermaidZoomReset: 'リセット',
|
||||
mermaidZoomHint: 'スクロールで拡大 ・ ドラッグで移動 ・ Esc で閉じる',
|
||||
refresh: '更新',
|
||||
showInFolder: 'フォルダーで表示',
|
||||
refreshUnsavedConfirm: 'ディスク上のファイルが変更されました。更新すると未保存の編集が失われます。続行しますか?',
|
||||
save: '保存',
|
||||
saved: '保存済み',
|
||||
unsaved: '未保存',
|
||||
|
||||
@@ -79,6 +79,8 @@ export const ko: Record<string, string> = {
|
||||
mermaidZoomReset: '초기화',
|
||||
mermaidZoomHint: '휠로 확대/축소 · 드래그로 이동 · Esc로 닫기',
|
||||
refresh: '새로고침',
|
||||
showInFolder: '폴더에서 표시',
|
||||
refreshUnsavedConfirm: '디스크의 파일이 변경되었습니다. 새로고침하면 저장되지 않은 편집 내용이 손실됩니다. 계속하시겠습니까?',
|
||||
save: '저장',
|
||||
saved: '저장됨',
|
||||
unsaved: '저장 안 됨',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -83,6 +83,8 @@ export const ru: Record<string, string> = {
|
||||
mermaidZoomReset: 'Сбросить',
|
||||
mermaidZoomHint: 'Колесо — масштаб · перетаскивание — панорама · Esc — закрыть',
|
||||
refresh: 'Обновить',
|
||||
showInFolder: 'Показать в папке',
|
||||
refreshUnsavedConfirm: 'Файл изменился на диске. Обновление отбросит несохранённые изменения. Продолжить?',
|
||||
save: 'Сохранить',
|
||||
saved: 'Сохранено',
|
||||
unsaved: 'Не сохранено',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -87,6 +87,8 @@ export const th: Record<string, string> = {
|
||||
mermaidZoomReset: 'รีเซ็ต',
|
||||
mermaidZoomHint: 'เลื่อนเพื่อซูม · ลากเพื่อแพน · Esc เพื่อปิด',
|
||||
refresh: 'รีเฟรช',
|
||||
showInFolder: 'แสดงในโฟลเดอร์',
|
||||
refreshUnsavedConfirm: 'ไฟล์เปลี่ยนบนดิสก์ การรีเฟรชจะทิ้งการแก้ไขที่ยังไม่ได้บันทึก ดำเนินการต่อหรือไม่',
|
||||
save: 'บันทึก',
|
||||
saved: 'บันทึกแล้ว',
|
||||
unsaved: 'ยังไม่ได้บันทึก',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -102,6 +102,8 @@ export const zhHK: Record<string, string> = {
|
||||
mermaidZoomReset: '重設',
|
||||
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
|
||||
refresh: '重新整理',
|
||||
showInFolder: '在文件夾中顯示',
|
||||
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
|
||||
save: '儲存',
|
||||
saved: '已儲存',
|
||||
unsaved: '未儲存',
|
||||
|
||||
@@ -102,6 +102,8 @@ export const zhMO: Record<string, string> = {
|
||||
mermaidZoomReset: '重設',
|
||||
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
|
||||
refresh: '重新整理',
|
||||
showInFolder: '在文件夾中顯示',
|
||||
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
|
||||
save: '儲存',
|
||||
saved: '已儲存',
|
||||
unsaved: '未儲存',
|
||||
|
||||
@@ -102,6 +102,8 @@ export const zhTW: Record<string, string> = {
|
||||
mermaidZoomReset: '重設',
|
||||
mermaidZoomHint: '滾輪縮放 · 拖曳平移 · Esc 關閉',
|
||||
refresh: '重新整理',
|
||||
showInFolder: '在資料夾中顯示',
|
||||
refreshUnsavedConfirm: '檔案已在磁碟上變更。重新整理會丟失尚未儲存的編輯。繼續?',
|
||||
save: '儲存',
|
||||
saved: '已儲存',
|
||||
unsaved: '未儲存',
|
||||
|
||||
+5
-8
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user