dsh-better-workspace 0.35.0 → 0.36.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +829 -142
package/src/client.js
CHANGED
|
@@ -125,8 +125,6 @@ window.__ModuleLoader__.load({
|
|
|
125
125
|
'settings.sessionLimit': "展开时显示会话数",
|
|
126
126
|
'settings.sessionLimit.all': "全部",
|
|
127
127
|
'settings.sessionLimit.hint': "展开工作区时先只显示最近的部分会话,其余收进「展开 {n} 个会话」一行,点击展开该组全部;置顶、进行中和等待你的会话始终显示。全部 = 不限量(默认)。",
|
|
128
|
-
'settings.sessionMenu': "会话右键菜单",
|
|
129
|
-
'settings.sessionMenu.hint': "开启时右键点击会话行即可打开操作菜单(包含官方的置顶、归档、停止并归档等);关闭后改为在行尾悬停显示官方样式的 ⋯ 按钮,点击打开同一个菜单。默认开启。",
|
|
130
128
|
'settings.graft.title': "自动标题保留分组前缀",
|
|
131
129
|
'settings.graft.hint': "「随对话更新的标题」实验功能会把会话改名成不带「/」分组的新名字,分组会因此丢失。开启后本插件视图保留原「/」前缀、只替换最后一段(如 A/xxx 自动变成 A/yyyy)。手动重命名不受影响。默认开启。",
|
|
132
130
|
'settings.rowActions': "会话行悬停按钮",
|
|
@@ -303,8 +301,6 @@ window.__ModuleLoader__.load({
|
|
|
303
301
|
'settings.sessionLimit': "Sessions shown when expanded",
|
|
304
302
|
'settings.sessionLimit.all': "All",
|
|
305
303
|
'settings.sessionLimit.hint': "An expanded workspace first shows only its most recent sessions, the rest behind one Show-more row; pinned, running and waiting-for-you sessions always stay visible. All = no limit (default).",
|
|
306
|
-
'settings.sessionMenu': "Session context menu",
|
|
307
|
-
'settings.sessionMenu.hint': "On: right-click a session row to open its action menu (including the official pin, archive, and stop-and-archive). Off: the row shows the official-style ⋯ button on hover instead, opening the same menu. On by default.",
|
|
308
304
|
'settings.graft.title': "Auto titles keep the group prefix",
|
|
309
305
|
'settings.graft.hint': "The 'titles that follow the conversation' experiment renames sessions to names without the '/' group, which dissolves the grouping. On, this view keeps the original '/' prefix and swaps only the last segment (A/xxx becomes A/yyyy). Manual renames are unaffected. On by default.",
|
|
310
306
|
'settings.rowActions': "Session row hover buttons",
|
|
@@ -489,8 +485,6 @@ window.__ModuleLoader__.load({
|
|
|
489
485
|
'settings.sessionLimit': "عدد الجلسات عند التوسيع",
|
|
490
486
|
'settings.sessionLimit.all': "الكل",
|
|
491
487
|
'settings.sessionLimit.hint': "تعرض مساحة العمل الموسعة أحدث الجلسات أولاً، والبقية خلف سطر عرض المزيد؛ الجلسات المثبتة والجارية والمنتظرة لك تبقى مرئية دائماً. الكل = بلا حد (افتراضي).",
|
|
492
|
-
'settings.sessionMenu': "قائمة النقر الأيمن للجلسة",
|
|
493
|
-
'settings.sessionMenu.hint': "عند التفعيل: انقر بزر الفأرة الأيمن على سطر الجلسة لفتح قائمة الإجراءات (بما في ذلك التثبيت والأرشفة الرسمية). عند الإيقاف: يظهر زر ⋯ بأسلوب رسمي عند تمرير المؤشر يفتح القائمة نفسها. مفعّل افتراضياً.",
|
|
494
488
|
'settings.graft.title': "عناوين تلقائية تحتفظ ببادئة المجموعة",
|
|
495
489
|
'settings.graft.hint': "تجربة 'العناوين التي تتبع المحادثة' تعيد تسمية الجلسات بأسماء بدون مجموعة '/'، فيختفي التقسيم. عند التفعيل يحتفظ هذا العرض بالبادئة الأصلية ويستبدل المقطع الأخير فقط (A/xxx يصبح A/yyyy). إعادة التسمية اليدوية لا تتأثر. مفعّل افتراضيًا.",
|
|
496
490
|
'settings.rowActions': "أزرار تمرير سطر الجلسة",
|
|
@@ -667,8 +661,6 @@ window.__ModuleLoader__.load({
|
|
|
667
661
|
'settings.sessionLimit': "Sitzungen beim Aufklappen",
|
|
668
662
|
'settings.sessionLimit.all': "Alle",
|
|
669
663
|
'settings.sessionLimit.hint': "Ein aufgeklappter Arbeitsbereich zeigt zuerst nur die neuesten Sitzungen, der Rest hinter einer Mehr-anzeigen-Zeile; angeheftete, laufende und auf Sie wartende Sitzungen bleiben immer sichtbar. Alle = unbegrenzt (Standard).",
|
|
670
|
-
'settings.sessionMenu': "Sitzungs-Kontextmenü",
|
|
671
|
-
'settings.sessionMenu.hint': "Ein: Rechtsklick auf eine Sitzungszeile öffnet das Aktionsmenü (inklusive offiziellem Anheften, Archivieren und Stoppen-und-Archivieren). Aus: beim Daraufzeigen erscheint stattdessen der offizielle ⋯-Knopf am Zeilenende und öffnet dasselbe Menü. Standardmäßig ein.",
|
|
672
664
|
'settings.graft.title': "Automatische Titel behalten den Gruppenpräfix",
|
|
673
665
|
'settings.graft.hint': "Das Experiment 'Titel, die der Unterhaltung folgen' benennt Sitzungen ohne '/'-Gruppe um, wodurch die Gruppierung verloren geht. Aktiv behält diese Ansicht den ursprünglichen Präfix und tauscht nur das letzte Segment (A/xxx wird A/yyyy). Manuelle Umbenennungen sind nicht betroffen. Standardmäßig aktiv.",
|
|
674
666
|
'settings.rowActions': "Sitzungszeilen-Schnellknöpfe",
|
|
@@ -845,8 +837,6 @@ window.__ModuleLoader__.load({
|
|
|
845
837
|
'settings.sessionLimit': "Sessions affichées à l’ouverture",
|
|
846
838
|
'settings.sessionLimit.all': "Tout",
|
|
847
839
|
'settings.sessionLimit.hint': "Un espace ouvert montre d’abord ses sessions les plus récentes, le reste derrière une ligne afficher-plus ; les sessions épinglées, en cours et en attente restent toujours visibles. Tout = sans limite (par défaut).",
|
|
848
|
-
'settings.sessionMenu': "Menu contextuel des sessions",
|
|
849
|
-
'settings.sessionMenu.hint': "Activé : un clic droit sur une ligne de session ouvre son menu d’actions (épinglage, archivage et arrêt-avec-archivage officiels inclus). Désactivé : la ligne affiche plutôt le bouton ⋯ officiel au survol, ouvrant le même menu. Activé par défaut.",
|
|
850
840
|
'settings.graft.title': "Les titres automatiques gardent le préfixe de groupe",
|
|
851
841
|
'settings.graft.hint': "L'expérience 'titres qui suivent la conversation' renomme les sessions sans le groupe '/', ce qui dissout le groupement. Activé, cette vue conserve le préfixe d'origine et ne remplace que le dernier segment (A/xxx devient A/yyyy). Les renommages manuels ne sont pas affectés. Activé par défaut.",
|
|
852
842
|
'settings.rowActions': "Boutons de survol des sessions",
|
|
@@ -1023,8 +1013,6 @@ window.__ModuleLoader__.load({
|
|
|
1023
1013
|
'settings.sessionLimit': "विस्तार पर दिखने वाले सेशन",
|
|
1024
1014
|
'settings.sessionLimit.all': "सभी",
|
|
1025
1015
|
'settings.sessionLimit.hint': "विस्तारित वर्कस्पेस पहले केवल हाल के सेशन दिखाता है, बाकी एक और-दिखाएँ पंक्ति के पीछे; पिन, चल रहे और आपकी प्रतीक्षा में सेशन सदा दिखते हैं। सभी = असीमित (डिफ़ॉल्ट)।",
|
|
1026
|
-
'settings.sessionMenu': "सेशन संदर्भ मेनू",
|
|
1027
|
-
'settings.sessionMenu.hint': "चालू: सेशन पंक्ति पर राइट-क्लिक करने से उसका क्रिया मेनू खुलता है (आधिकारिक पिन, आर्काइव और रोककर-आर्काइव सहित)। बंद: पंक्ति पर होवर करने पर आधिकारिक शैली का ⋯ बटन दिखता है, जो वही मेनू खोलता है। डिफ़ॉल्ट रूप से चालू।",
|
|
1028
1016
|
'settings.graft.title': "स्वतः शीर्षक समूह उपसर्ग रखते हैं",
|
|
1029
1017
|
'settings.graft.hint': "'बातचीत के अनुसार शीर्षक' प्रयोग सत्रों का नाम बिना '/' समूह के बदल देता है, जिससे समूहन टूट जाता है। चालू होने पर यह दृश्य मूल उपसर्ग रखता है और केवल अंतिम खंड बदलता है (A/xxx → A/yyyy)। मैन्युअल नाम बदलना प्रभावित नहीं होता। डिफ़ॉल्ट रूप से चालू।",
|
|
1030
1018
|
'settings.rowActions': "सेशन पंक्ति होवर बटन",
|
|
@@ -1201,8 +1189,6 @@ window.__ModuleLoader__.load({
|
|
|
1201
1189
|
'settings.sessionLimit': "Sesi saat dibuka",
|
|
1202
1190
|
'settings.sessionLimit.all': "Semua",
|
|
1203
1191
|
'settings.sessionLimit.hint': "Ruang kerja yang dibuka pertama menampilkan sesi terbaru, sisanya di balik satu baris tampilkan-lebih; sesi pin, berjalan, dan menunggu Anda selalu terlihat. Semua = tanpa batas (bawaan).",
|
|
1204
|
-
'settings.sessionMenu': "Menu klik kanan sesi",
|
|
1205
|
-
'settings.sessionMenu.hint': "Aktif: klik kanan pada baris sesi untuk membuka menu aksinya (termasuk pin, arsip, dan henti-dan-arsip resmi). Nonaktif: baris menampilkan tombol ⋯ gaya resmi saat dihover, membuka menu yang sama. Aktif secara bawaan.",
|
|
1206
1192
|
'settings.graft.title': "Judul otomatis mempertahankan awalan grup",
|
|
1207
1193
|
'settings.graft.hint': "Eksperimen 'judul yang mengikuti percakapan' mengganti nama sesi tanpa grup '/', sehingga pengelompokan hilang. Saat aktif, tampilan ini mempertahankan awalan asli dan hanya mengganti segmen terakhir (A/xxx menjadi A/yyyy). Penggantian nama manual tidak terpengaruh. Aktif secara bawaan.",
|
|
1208
1194
|
'settings.rowActions': "Tombol hover baris sesi",
|
|
@@ -1379,8 +1365,6 @@ window.__ModuleLoader__.load({
|
|
|
1379
1365
|
'settings.sessionLimit': "Sessioni mostrate all’apertura",
|
|
1380
1366
|
'settings.sessionLimit.all': "Tutte",
|
|
1381
1367
|
'settings.sessionLimit.hint': "Un’area aperta mostra prima le sessioni più recenti, il resto dietro una riga mostra-altro; sessioni fissate, in corso e in attesa restano sempre visibili. Tutte = senza limiti (predefinito).",
|
|
1382
|
-
'settings.sessionMenu': "Menu contestuale sessione",
|
|
1383
|
-
'settings.sessionMenu.hint': "Attivo: il clic destro su una riga di sessione apre il menu azioni (con pin, archiviazione e ferma-e-archivia ufficiali). Disattivo: la riga mostra il pulsante ⋯ ufficiale al passaggio, aprendo lo stesso menu. Attivo per impostazione predefinita.",
|
|
1384
1368
|
'settings.graft.title': "I titoli automatici conservano il prefisso del gruppo",
|
|
1385
1369
|
'settings.graft.hint': "L'esperimento 'titoli che seguono la conversazione' rinomina le sessioni senza il gruppo '/', sciogliendo il raggruppamento. Attivo, questa vista conserva il prefisso originale e sostituisce solo l'ultimo segmento (A/xxx diventa A/yyyy). Le rinomine manuali non sono influenzate. Attivo per impostazione predefinita.",
|
|
1386
1370
|
'settings.rowActions': "Pulsanti al passaggio della sessione",
|
|
@@ -1557,8 +1541,6 @@ window.__ModuleLoader__.load({
|
|
|
1557
1541
|
'settings.sessionLimit': "展開時に表示するセッション数",
|
|
1558
1542
|
'settings.sessionLimit.all': "すべて",
|
|
1559
1543
|
'settings.sessionLimit.hint': "展開したワークスペースは最近のセッションを先に表示し、残りは「さらに表示」行の後ろに収まります。ピン留め・実行中・対応待ちのセッションは常に表示。すべて = 無制限(既定)。",
|
|
1560
|
-
'settings.sessionMenu': "セッション右クリックメニュー",
|
|
1561
|
-
'settings.sessionMenu.hint': "オン:セッション行を右クリックすると操作メニューを開きます(公式のピン留め・アーカイブ・停止してアーカイブを含む)。オフ:行にカーソルを合わせると公式スタイルの ⋯ ボタンが表示され、同じメニューを開きます。既定はオン。",
|
|
1562
1544
|
'settings.graft.title': "自動タイトルはグループ接頭辞を保持",
|
|
1563
1545
|
'settings.graft.hint': "「会話に合わせて更新されるタイトル」実験機能は「/」グループなしの名前にセッションを改名するため、グルーピングが失われます。オンにすると、このビューは元の「/」接頭辞を保持し最終セグメントだけを差し替えます(A/xxx → A/yyyy)。手動リネームには影響しません。デフォルトでオン。",
|
|
1564
1546
|
'settings.rowActions': "セッション行のホバーボタン",
|
|
@@ -1735,8 +1717,6 @@ window.__ModuleLoader__.load({
|
|
|
1735
1717
|
'settings.sessionLimit': "펼칠 때 표시할 세션",
|
|
1736
1718
|
'settings.sessionLimit.all': "전체",
|
|
1737
1719
|
'settings.sessionLimit.hint': "펼친 작업 공간은 최근 세션을 먼저 보여주고 나머지는 더보기 행 뒤에 둡니다. 고정, 실행 중, 대기 중인 세션은 항상 표시됩니다. 전체 = 제한 없음(기본값).",
|
|
1738
|
-
'settings.sessionMenu': "세션 우클릭 메뉴",
|
|
1739
|
-
'settings.sessionMenu.hint': "켬: 세션 행을 우클릭하면 작업 메뉴가 열립니다(공식 고정, 보관, 중지 후 보관 포함). 끔: 행에 마우스를 올리면 공식 스타일의 ⋯ 버튼이 표시되어 같은 메뉴를 엽니다. 기본값은 켬.",
|
|
1740
1720
|
'settings.graft.title': "자동 제목은 그룹 접두어를 유지",
|
|
1741
1721
|
'settings.graft.hint': "'대화에 따라 업데이트되는 제목' 실험 기능이 '/' 그룹 없는 이름으로 세션 이름을 바꿔 그룹핑이 사라집니다. 켜면 이 뷰는 원래 '/' 접두어를 유지하고 마지막 세그먼트만 교체합니다(A/xxx → A/yyyy). 수동 이름 바꾸기는 영향을 받지 않습니다. 기본적으로 켬.",
|
|
1742
1722
|
'settings.rowActions': "세션 행 호버 버튼",
|
|
@@ -1913,8 +1893,6 @@ window.__ModuleLoader__.load({
|
|
|
1913
1893
|
'settings.sessionLimit': "Sessies bij uitklappen",
|
|
1914
1894
|
'settings.sessionLimit.all': "Alles",
|
|
1915
1895
|
'settings.sessionLimit.hint': "Een uitgeklapte werkruimte toont eerst de nieuwste sessies, de rest achter een toon-meer rij; vastgemaakte, lopende en op u wachtende sessies blijven altijd zichtbaar. Alles = onbeperkt (standaard).",
|
|
1916
|
-
'settings.sessionMenu': "Sessie-contextmenu",
|
|
1917
|
-
'settings.sessionMenu.hint': "Aan: rechtermuisklik op een sessierij opent het actiemenu (inclusief officieel vastmaken, archiveren en stoppen-en-archiveren). Uit: bij aanwijzen verschijnt in plaats daarvan de officiële ⋯-knop aan het rij-einde, die hetzelfde menu opent. Standaard aan.",
|
|
1918
1896
|
'settings.graft.title': "Automatische titels behouden het groepsprefix",
|
|
1919
1897
|
'settings.graft.hint': "Het experiment 'titels die het gesprek volgen' hernoemt sessies naar namen zonder de '/'-groep, waardoor de groepering verloren gaat. Aan: deze weergave behoudt het oorspronkelijke prefix en vervangt alleen het laatste segment (A/xxx wordt A/yyyy). Handmatige hernoemingen zijn niet getroffen. Standaard aan.",
|
|
1920
1898
|
'settings.rowActions': "Sessierij-snelknoppen",
|
|
@@ -2091,8 +2069,6 @@ window.__ModuleLoader__.load({
|
|
|
2091
2069
|
'settings.sessionLimit': "Sesje po rozwinięciu",
|
|
2092
2070
|
'settings.sessionLimit.all': "Wszystkie",
|
|
2093
2071
|
'settings.sessionLimit.hint': "Rozwinięty obszar pokazuje najpierw najnowsze sesje, reszta czeka za wierszem pokaż-więcej; przypięte, uruchomione i czekające sesje są zawsze widoczne. Wszystkie = bez limitu (domyślnie).",
|
|
2094
|
-
'settings.sessionMenu': "Menu kontekstowe sesji",
|
|
2095
|
-
'settings.sessionMenu.hint': "Włączone: kliknięcie prawym przyciskiem wiersza sesji otwiera jego menu akcji (w tym oficjalne przypinanie, archiwizację i zatrzymanie z archiwizacją). Wyłączone: przy najechaniu wiersz pokazuje oficjalny przycisk ⋯ otwierający to samo menu. Domyślnie włączone.",
|
|
2096
2072
|
'settings.graft.title': "Automatyczne tytuły zachowują prefiks grupy",
|
|
2097
2073
|
'settings.graft.hint': "Eksperyment 'tytuły podążające za rozmową' zmienia nazwy sesji na bez grupy '/', co rozbija grupowanie. Włączone: ten widok zachowuje oryginalny prefiks i zamienia tylko ostatni segment (A/xxx staje się A/yyyy). Ręczne zmiany nazwy nie są objęte. Domyślnie włączone.",
|
|
2098
2074
|
'settings.rowActions': "Przyciski najechania wiersza sesji",
|
|
@@ -2269,8 +2245,6 @@ window.__ModuleLoader__.load({
|
|
|
2269
2245
|
'settings.sessionLimit': "Sessões ao expandir",
|
|
2270
2246
|
'settings.sessionLimit.all': "Todas",
|
|
2271
2247
|
'settings.sessionLimit.hint': "Um espaço expandido mostra primeiro as sessões mais recentes, o restante atrás de uma linha mostrar-mais; sessões fixadas, em execução e aguardando você permanecem visíveis. Todas = sem limite (predefinição).",
|
|
2272
|
-
'settings.sessionMenu': "Menu de contexto da sessão",
|
|
2273
|
-
'settings.sessionMenu.hint': "Ligado: clicar com o botão direito numa linha de sessão abre o menu de ações (incluindo fixar, arquivar e parar-e-arquivar oficiais). Desligado: a linha mostra o botão ⋯ oficial ao passar o cursor, abrindo o mesmo menu. Ligado por predefinição.",
|
|
2274
2248
|
'settings.graft.title': "Títulos automáticos mantêm o prefixo do grupo",
|
|
2275
2249
|
'settings.graft.hint': "O experimento 'títulos que acompanham a conversa' renomeia sessões sem o grupo '/', dissolvendo o agrupamento. Ativo: esta visão mantém o prefixo original e troca apenas o último segmento (A/xxx vira A/yyyy). Renomeações manuais não são afetadas. Ativo por padrão.",
|
|
2276
2250
|
'settings.rowActions': "Botões de passagem da sessão",
|
|
@@ -2447,8 +2421,6 @@ window.__ModuleLoader__.load({
|
|
|
2447
2421
|
'settings.sessionLimit': "Сессий при разворачивании",
|
|
2448
2422
|
'settings.sessionLimit.all': "Все",
|
|
2449
2423
|
'settings.sessionLimit.hint': "Развёрнутая область сначала показывает последние сессии, остальные — за строкой «показать ещё»; закреплённые, идущие и ожидающие вас сессии видны всегда. Все = без ограничения (по умолчанию).",
|
|
2450
|
-
'settings.sessionMenu': "Контекстное меню сессии",
|
|
2451
|
-
'settings.sessionMenu.hint': "Вкл: правый клик по строке сессии открывает меню действий (включая официальные закрепление, архивацию и «остановить и архивировать»). Выкл: при наведении в строке появляется официальная кнопка ⋯, открывающая то же меню. Включено по умолчанию.",
|
|
2452
2424
|
'settings.graft.title': "Автозаголовки сохраняют префикс группы",
|
|
2453
2425
|
'settings.graft.hint': "Эксперимент «заголовки, следующие за беседой» переименовывает сессии в имена без группы «/», из-за чего группировка теряется. Включено: этот вид сохраняет исходный префикс «/» и заменяет только последний сегмент (A/xxx становится A/yyyy). Ручные переименования не затрагиваются. Включено по умолчанию.",
|
|
2454
2426
|
'settings.rowActions': "Кнопки наведения строки сессии",
|
|
@@ -2625,8 +2597,6 @@ window.__ModuleLoader__.load({
|
|
|
2625
2597
|
'settings.sessionLimit': "Sessioner vid utfällning",
|
|
2626
2598
|
'settings.sessionLimit.all': "Alla",
|
|
2627
2599
|
'settings.sessionLimit.hint': "En utfälld arbetsyta visar först de senaste sessionerna, resten bakom en visa-mer-rad; fästa, pågående och väntande sessioner syns alltid. Alla = obegränsat (standard).",
|
|
2628
|
-
'settings.sessionMenu': "Sessionens högerklicksmeny",
|
|
2629
|
-
'settings.sessionMenu.hint': "På: högerklick på en sessionsrad öppnar dess åtgärdsmeny (inklusive officiell fästning, arkivering och stoppa-och-arkivera). Av: raden visar istället den officiella ⋯-knappen vid hovring, som öppnar samma meny. På som standard.",
|
|
2630
2600
|
'settings.graft.title': "Automatiska titlar behåller gruppprefixet",
|
|
2631
2601
|
'settings.graft.hint': "Experimentet 'titlar som följer samtalet' döper om sessioner till namn utan '/'-gruppen, vilket löser upp grupperingen. På: den här vyn behåller det ursprungliga prefixet och byter bara ut sista segmentet (A/xxx blir A/yyyy). Manuella namnbyten påverkas inte. På som standard.",
|
|
2632
2602
|
'settings.rowActions': "Hovringsknappar för sessionsraden",
|
|
@@ -2803,8 +2773,6 @@ window.__ModuleLoader__.load({
|
|
|
2803
2773
|
'settings.sessionLimit': "จำนวนเซสชันเมื่อกาง",
|
|
2804
2774
|
'settings.sessionLimit.all': "ทั้งหมด",
|
|
2805
2775
|
'settings.sessionLimit.hint': "พื้นที่ที่กางจะแสดงเซสชันล่าสุดก่อน ส่วนที่เหลืออยู่หลังแถวแสดงเพิ่ม เซสชันที่ปักหมุด กำลังทำงาน และรอคุณจะแสดงตลอด ทั้งหมด = ไม่จำกัด (ค่าเริ่มต้น)",
|
|
2806
|
-
'settings.sessionMenu': "เมนูคลิกขวาของเซสชัน",
|
|
2807
|
-
'settings.sessionMenu.hint': "เปิด: คลิกขวาที่แถวเซสชันเพื่อเปิดเมนูการกระทำ (รวมปักหมุด, เก็บถาวร และหยุด-แล้ว-เก็บถาวรแบบทางการ) ปิด: เมื่อชี้ที่แถวจะแสดงปุ่ม ⋯ สไตล์ทางการเปิดเมนูเดียวกันแทน เปิดตามค่าเริ่มต้น",
|
|
2808
2776
|
'settings.graft.title': "ชื่ออัตโนมัติคงคำนำหน้ากลุ่ม",
|
|
2809
2777
|
'settings.graft.hint': "ฟีเจอร์ทดลอง 'ชื่อที่ตามการสนทนา' เปลี่ยนชื่อเซสชันเป็นชื่อที่ไม่มีกลุ่ม '/' ทำให้การจัดกลุ่มหายไป เมื่อเปิด มุมมองนี้จะคงคำนำหน้าเดิมและเปลี่ยนเฉพาะส่วนท้าย (A/xxx กลายเป็น A/yyyy) การเปลี่ยนชื่อด้วยมือไม่ได้รับผลกระทบ เปิดตามค่าเริ่มต้น",
|
|
2810
2778
|
'settings.rowActions': "ปุ่มเมื่อชี้แถวเซสชัน",
|
|
@@ -2981,8 +2949,6 @@ window.__ModuleLoader__.load({
|
|
|
2981
2949
|
'settings.sessionLimit': "Açıldığında gösterilecek oturum",
|
|
2982
2950
|
'settings.sessionLimit.all': "Tümü",
|
|
2983
2951
|
'settings.sessionLimit.hint': "Açılan çalışma alanı önce en yeni oturumları gösterir, kalanı bir daha-göster satırının ardındadır; sabitlenmiş, çalışan ve sizi bekleyen oturumlar her zaman görünür. Tümü = sınırsız (varsayılan).",
|
|
2984
|
-
'settings.sessionMenu': "Oturum sağ tık menüsü",
|
|
2985
|
-
'settings.sessionMenu.hint': "Açık: oturum satırına sağ tıklayınca eylem menüsü açılır (resmî sabitleme, arşivleme ve durdurup-arşivleme dahil). Kapalı: satırın üzerine gelince resmî ⋯ düğmesi görünür ve aynı menüyü açar. Varsayılan açık.",
|
|
2986
2952
|
'settings.graft.title': "Otomatik başlıklar grup önekini korur",
|
|
2987
2953
|
'settings.graft.hint': "Deneme özelliği 'konuşmayı izleyen başlıklar' oturumları '/' grubu olmadan yeniden adlandırır, bu da gruplamayı çözer. Açıkken bu görünüm özgün öneki korur ve yalnızca son bölümü değiştirir (A/xxx, A/yyyy olur). Manuel yeniden adlandırmalar etkilenmez. Varsayılan olarak açık.",
|
|
2988
2954
|
'settings.rowActions': "Oturum satırı düğmeleri",
|
|
@@ -3159,8 +3125,6 @@ window.__ModuleLoader__.load({
|
|
|
3159
3125
|
'settings.sessionLimit': "Số phiên khi mở rộng",
|
|
3160
3126
|
'settings.sessionLimit.all': "Tất cả",
|
|
3161
3127
|
'settings.sessionLimit.hint': "Không gian được mở rộng sẽ hiện các phiên gần đây trước, phần còn lại nằm sau một hàng xem-thêm; phiên ghim, đang chạy và đang chờ bạn luôn hiển thị. Tất cả = không giới hạn (mặc định).",
|
|
3162
|
-
'settings.sessionMenu': "Menu chuột phải phiên",
|
|
3163
|
-
'settings.sessionMenu.hint': "Bật: nhấp chuột phải vào dòng phiên để mở menu thao tác (bao gồm ghim, lưu trữ và dừng-rồi-lưu trữ chính thức). Tắt: dòng hiển thị nút ⋯ kiểu chính thức khi di chuột, mở cùng menu đó. Bật theo mặc định.",
|
|
3164
3128
|
'settings.graft.title': "Tiêu đề tự động giữ tiền tố nhóm",
|
|
3165
3129
|
'settings.graft.hint': "Thử nghiệm 'tiêu đề cập nhật theo hội thoại' đổi tên phiên thành tên không có nhóm '/', làm mất phân nhóm. Bật: khung nhìn này giữ tiền tố gốc và chỉ thay đoạn cuối (A/xxx thành A/yyyy). Đổi tên thủ công không bị ảnh hưởng. Bật theo mặc định.",
|
|
3166
3130
|
'settings.rowActions': "Nút hover dòng phiên",
|
|
@@ -3337,8 +3301,6 @@ window.__ModuleLoader__.load({
|
|
|
3337
3301
|
'settings.sessionLimit': "展開時顯示會話數",
|
|
3338
3302
|
'settings.sessionLimit.all': "全部",
|
|
3339
3303
|
'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;置頂、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
|
|
3340
|
-
'settings.sessionMenu': "會話右鍵選單",
|
|
3341
|
-
'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、封存、停止並封存等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
|
|
3342
3304
|
'settings.graft.title': "自動標題保留分組前綴",
|
|
3343
3305
|
'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本插件視圖保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
|
|
3344
3306
|
'settings.rowActions': "會話行懸停按鈕",
|
|
@@ -3515,8 +3477,6 @@ window.__ModuleLoader__.load({
|
|
|
3515
3477
|
'settings.sessionLimit': "展開時顯示會話數",
|
|
3516
3478
|
'settings.sessionLimit.all': "全部",
|
|
3517
3479
|
'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;置頂、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
|
|
3518
|
-
'settings.sessionMenu': "會話右鍵選單",
|
|
3519
|
-
'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、歸檔、停止並歸檔等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
|
|
3520
3480
|
'settings.graft.title': "自動標題保留分組前綴",
|
|
3521
3481
|
'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本外掛檢視保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
|
|
3522
3482
|
'settings.rowActions': "會話行懸停按鈕",
|
|
@@ -3693,8 +3653,6 @@ window.__ModuleLoader__.load({
|
|
|
3693
3653
|
'settings.sessionLimit': "展開時顯示會話數",
|
|
3694
3654
|
'settings.sessionLimit.all': "全部",
|
|
3695
3655
|
'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;釘選、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
|
|
3696
|
-
'settings.sessionMenu': "會話右鍵選單",
|
|
3697
|
-
'settings.sessionMenu.hint': "開啟時右鍵點擊會話列即可開啟動作選單(包含官方的釘選、封存、停止並封存等);關閉後改為在列尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
|
|
3698
3656
|
'settings.graft.title': "自動標題保留分組前綴",
|
|
3699
3657
|
'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本插件檢視保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
|
|
3700
3658
|
'settings.rowActions': "會話列懸停按鈕",
|
|
@@ -4527,7 +4485,7 @@ window.__ModuleLoader__.load({
|
|
|
4527
4485
|
return out
|
|
4528
4486
|
}
|
|
4529
4487
|
|
|
4530
|
-
function buildTree(items, groupMode, explicitWs) {
|
|
4488
|
+
function buildTree(items, groupMode, explicitWs, folderOrder) {
|
|
4531
4489
|
const list = items || []
|
|
4532
4490
|
// Issue #7: 'disk' nests by disk only, 'disk-slash' adds the name
|
|
4533
4491
|
// layer on top (the historical workspaceSlash-on shape), 'slash' runs
|
|
@@ -4606,11 +4564,76 @@ window.__ModuleLoader__.load({
|
|
|
4606
4564
|
}
|
|
4607
4565
|
ensure(segs.slice(0, -1)).workspaces.push(entry)
|
|
4608
4566
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4567
|
+
// Group order: the NAME sort is only the seed. v0.36.4 keeps a
|
|
4568
|
+
// persisted per-parent name list (folderOrder) so a dragged group
|
|
4569
|
+
// keeps its place — before this the drop committed the registry
|
|
4570
|
+
// order but the renderer re-sorted by name, which is why dragging a
|
|
4571
|
+
// group row looked like a no-op. Names absent from the stored list
|
|
4572
|
+
// (a just-created group) sort after the known ones by locale.
|
|
4573
|
+
// v0.36.5: the SAME list may also carry "w:<workspaceId>" tokens,
|
|
4574
|
+
// which interleave a BARE workspace row with the group rows of its
|
|
4575
|
+
// level. Before this a level always rendered every folder first and
|
|
4576
|
+
// every workspace after, so a top-level workspace could never be
|
|
4577
|
+
// dropped "as the first row" — the probe showed the commit landing
|
|
4578
|
+
// but the row staying at the bottom ("插到第一个永远失败").
|
|
4579
|
+
// Tokens absent from the list (a new member, a workspace that just
|
|
4580
|
+
// moved in) append in the default order: groups first, then
|
|
4581
|
+
// workspaces in registry order.
|
|
4582
|
+
const orderOf = (parentKey) => {
|
|
4583
|
+
if (!folderOrder || typeof folderOrder !== 'object') return null
|
|
4584
|
+
const list = folderOrder[parentKey]
|
|
4585
|
+
return Array.isArray(list) ? list : null
|
|
4612
4586
|
}
|
|
4613
|
-
sortRec(
|
|
4587
|
+
const sortRec = (node, parentKey) => {
|
|
4588
|
+
const order = orderOf(parentKey)
|
|
4589
|
+
const names = order
|
|
4590
|
+
? order.filter((token) => typeof token === 'string' && token !== '' && !token.startsWith('w:'))
|
|
4591
|
+
: null
|
|
4592
|
+
if (names && names.length) {
|
|
4593
|
+
const rank = new Map()
|
|
4594
|
+
for (let i = 0; i < names.length; i += 1) rank.set(names[i], i)
|
|
4595
|
+
node.folders.sort((a, b) => {
|
|
4596
|
+
const ra = rank.has(a.name) ? rank.get(a.name) : names.length
|
|
4597
|
+
const rb = rank.has(b.name) ? rank.get(b.name) : names.length
|
|
4598
|
+
if (ra !== rb) return ra - rb
|
|
4599
|
+
return a.name.localeCompare(b.name, 'zh')
|
|
4600
|
+
})
|
|
4601
|
+
} else {
|
|
4602
|
+
node.folders.sort((a, b) => a.name.localeCompare(b.name, 'zh'))
|
|
4603
|
+
}
|
|
4604
|
+
// Level identity every workspace row carries into its drag payload:
|
|
4605
|
+
// the mixed reorder is siblings-only, so the drop gate compares it
|
|
4606
|
+
// with parentKeyOf(target group idPath).
|
|
4607
|
+
for (const w of node.workspaces) w.levelKey = parentKey
|
|
4608
|
+
// Mixed display sequence of THIS level: the persisted order first
|
|
4609
|
+
// (groups and bare workspaces interleaved), then whatever the list
|
|
4610
|
+
// does not mention, groups before workspaces.
|
|
4611
|
+
const folderByName = new Map(node.folders.map((child) => [child.name, child]))
|
|
4612
|
+
const wsById = new Map(node.workspaces.map((w) => [w.workspaceId, w]))
|
|
4613
|
+
const seq = []
|
|
4614
|
+
const seenFolders = new Set()
|
|
4615
|
+
const seenWorkspaces = new Set()
|
|
4616
|
+
for (const token of order || []) {
|
|
4617
|
+
if (typeof token !== 'string' || token === '') continue
|
|
4618
|
+
if (token.startsWith('w:')) {
|
|
4619
|
+
const id = token.slice(2)
|
|
4620
|
+
const entry = wsById.get(id)
|
|
4621
|
+
if (!entry || seenWorkspaces.has(id)) continue
|
|
4622
|
+
seq.push({ kind: 'ws', entry })
|
|
4623
|
+
seenWorkspaces.add(id)
|
|
4624
|
+
} else {
|
|
4625
|
+
const child = folderByName.get(token)
|
|
4626
|
+
if (!child || seenFolders.has(token)) continue
|
|
4627
|
+
seq.push({ kind: 'folder', node: child })
|
|
4628
|
+
seenFolders.add(token)
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
for (const child of node.folders) if (!seenFolders.has(child.name)) seq.push({ kind: 'folder', node: child })
|
|
4632
|
+
for (const w of node.workspaces) if (!seenWorkspaces.has(w.workspaceId)) seq.push({ kind: 'ws', entry: w })
|
|
4633
|
+
node.seq = seq
|
|
4634
|
+
for (const child of node.folders) sortRec(child, child.idPath)
|
|
4635
|
+
}
|
|
4636
|
+
sortRec(root, idPrefix)
|
|
4614
4637
|
return root
|
|
4615
4638
|
}
|
|
4616
4639
|
const topLevel = list.filter((w) => diskParentOf.get(w.workspaceId) === undefined)
|
|
@@ -4715,7 +4738,7 @@ window.__ModuleLoader__.load({
|
|
|
4715
4738
|
'.bw-rows-exits{position:fixed;inset:0;contain:strict;overflow:clip;pointer-events:none}',
|
|
4716
4739
|
'.bw-tree[data-bw-density="compact"]{--bw-row-h:28px;--bw-session-h:26px}',
|
|
4717
4740
|
'.bw-tree[data-bw-density="roomy"]{--bw-row-h:40px;--bw-session-h:38px}',
|
|
4718
|
-
'.bw-row{display:flex;align-items:center;gap:6px;height:var(--bw-row-h,34px);padding:0 8px;padding-inline-start:calc(8px + var(--bw-indent,0px));border-radius:var(--dsw-radius-md,8px);cursor:pointer;user-select:none;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative}',
|
|
4741
|
+
'.bw-row{display:flex;align-items:center;gap:6px;height:var(--bw-row-h,34px);padding:0 8px;padding-inline-start:calc(8px + var(--bw-indent,0px));border-radius:var(--dsw-radius-md,8px);cursor:pointer;user-select:none;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative;transition:height .15s ease-out,padding .15s ease-out}',
|
|
4719
4742
|
'.bw-drop-before::before{content:"";position:absolute;z-index:1;left:0;right:4px;top:-7px;height:12px;pointer-events:none;background:linear-gradient(55deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 0/5px 7px no-repeat,linear-gradient(125deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 5px/5px 7px no-repeat,linear-gradient(var(--dsw-alias-state-business-primary,#5b8def),var(--dsw-alias-state-business-primary,#5b8def)) 4px 5px/calc(100% - 4px) 2px no-repeat}',
|
|
4720
4743
|
'.bw-drop-after::after{content:"";position:absolute;z-index:1;left:0;right:4px;bottom:-7px;height:12px;pointer-events:none;background:linear-gradient(55deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 0/5px 7px no-repeat,linear-gradient(125deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 5px/5px 7px no-repeat,linear-gradient(var(--dsw-alias-state-business-primary,#5b8def),var(--dsw-alias-state-business-primary,#5b8def)) 4px 5px/calc(100% - 4px) 2px no-repeat}',
|
|
4721
4744
|
'.bw-drop-into{outline:1.5px dashed var(--dsw-alias-brand-primary,#5b8def);outline-offset:-1.5px}',
|
|
@@ -4723,6 +4746,15 @@ window.__ModuleLoader__.load({
|
|
|
4723
4746
|
// rim + a tinted wash so "this drop joins a group" reads stronger than
|
|
4724
4747
|
// the reorder dashes above.
|
|
4725
4748
|
'.bw-drop-into-strong{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px;background:color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 16%, transparent)}',
|
|
4749
|
+
// v0.36.1 edge-band reorder slot on group rows: the row SPLITS OPEN to
|
|
4750
|
+
// twice its height so "a row will land here" is visible without
|
|
4751
|
+
// staring for the thin official line. Both the height and the content
|
|
4752
|
+
// slide are transitioned (padding, which CAN animate — align-items
|
|
4753
|
+
// cannot, it teleports the content and feels janky), so the row
|
|
4754
|
+
// opens/closes like a drawer, not a flash. The gap fills with a faint
|
|
4755
|
+
// brand wash; the middle half of the row (drop-into) never opens.
|
|
4756
|
+
'.bw-slot-above{padding-top:var(--bw-row-h,34px);background:linear-gradient(to bottom, color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 10%, transparent) 0, transparent calc(var(--bw-row-h,34px)))}',
|
|
4757
|
+
'.bw-slot-below{padding-bottom:var(--bw-row-h,34px);background:linear-gradient(to top, color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 10%, transparent) 0, transparent calc(var(--bw-row-h,34px)))}',
|
|
4726
4758
|
// Session drag whose source sits in a group: a quiet dashed line at the
|
|
4727
4759
|
// bottom of the tree says "anywhere outside a group = out of it". Kept
|
|
4728
4760
|
// faint on purpose — the row outlines are the loud feedback.
|
|
@@ -5020,7 +5052,7 @@ window.__ModuleLoader__.load({
|
|
|
5020
5052
|
// LITERAL, not a spread of DEFAULT_APPEARANCE: init() is eval-harness
|
|
5021
5053
|
// friendly (no cross-section reference), and the smoke suite asserts the
|
|
5022
5054
|
// two stay in lockstep field by field.
|
|
5023
|
-
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} } }),
|
|
5055
|
+
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} }, folderOrder: {} }),
|
|
5024
5056
|
// NOTE: hydration REPLACES the state with the persisted whole value —
|
|
5025
5057
|
// init defaults never merge. Every action must tolerate a missing key
|
|
5026
5058
|
// (states persisted by older plugin versions lack sessionGroups), and
|
|
@@ -5080,6 +5112,19 @@ window.__ModuleLoader__.load({
|
|
|
5080
5112
|
},
|
|
5081
5113
|
// v0.21.0 collapsible Ungrouped block (issue #6).
|
|
5082
5114
|
setUngroupedOpen: (d, value) => { d.ungroupedOpen = value },
|
|
5115
|
+
// Persisted GROUP order (v0.36.4), one name list per PARENT identity
|
|
5116
|
+
// (a level's idPrefix for the root level, else the parent group's
|
|
5117
|
+
// idPath). Only sibling reorders are written here — a cross-parent
|
|
5118
|
+
// move is a rename, not an order change, and stays refused in the
|
|
5119
|
+
// drop handler. An empty list deletes the entry so the name sort
|
|
5120
|
+
// comes back for that parent.
|
|
5121
|
+
setFolderOrder: (d, parentKey, names) => {
|
|
5122
|
+
if (!d.folderOrder || typeof d.folderOrder !== 'object') d.folderOrder = {}
|
|
5123
|
+
const key = String(parentKey == null ? '' : parentKey)
|
|
5124
|
+
const clean = (Array.isArray(names) ? names : []).map((n) => String(n)).filter((n) => n !== '')
|
|
5125
|
+
if (clean.length === 0) delete d.folderOrder[key]
|
|
5126
|
+
else d.folderOrder[key] = clean
|
|
5127
|
+
},
|
|
5083
5128
|
// Explicit (possibly EMPTY) groups, v0.17.0. A group has always been a
|
|
5084
5129
|
// projection of "/" in member titles, so a group with no members simply
|
|
5085
5130
|
// did not exist and "create a group" had nothing to write. These two
|
|
@@ -6019,7 +6064,7 @@ window.__ModuleLoader__.load({
|
|
|
6019
6064
|
}
|
|
6020
6065
|
}
|
|
6021
6066
|
|
|
6022
|
-
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
|
|
6067
|
+
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dropSlot, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
|
|
6023
6068
|
const total = countWorkspaces(node)
|
|
6024
6069
|
const iconEl = iconOf(iconMode, expanded)
|
|
6025
6070
|
// Hidden status dots breathe on the icon; with the icon hidden (mode
|
|
@@ -6028,7 +6073,14 @@ window.__ModuleLoader__.load({
|
|
|
6028
6073
|
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
6029
6074
|
: iconEl
|
|
6030
6075
|
return E('div', {
|
|
6031
|
-
|
|
6076
|
+
// v0.36.4: an edge band opens the EMPTY reorder slot immediately —
|
|
6077
|
+
// the row grows by one row height on the dropped side, so the
|
|
6078
|
+
// insertion point is a real gap the block slides into (the user
|
|
6079
|
+
// report: a thin line is not "撑开让位").
|
|
6080
|
+
className: cls('bw-row', 'bw-container-row',
|
|
6081
|
+
dropSlot === 'before' && 'bw-slot-above',
|
|
6082
|
+
dropSlot === 'after' && 'bw-slot-below',
|
|
6083
|
+
dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6032
6084
|
'data-row-key': dataRowKey,
|
|
6033
6085
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
6034
6086
|
onClick: onToggle,
|
|
@@ -6048,13 +6100,17 @@ window.__ModuleLoader__.load({
|
|
|
6048
6100
|
)
|
|
6049
6101
|
}
|
|
6050
6102
|
|
|
6051
|
-
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart,
|
|
6103
|
+
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, hoverBody, dataRowKey, t }) {
|
|
6052
6104
|
const iconEl = iconOf(iconMode, sessionsOpen)
|
|
6053
6105
|
const iconChild = pulse
|
|
6054
6106
|
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
6055
6107
|
: iconEl
|
|
6056
6108
|
let row = E('div', {
|
|
6057
|
-
|
|
6109
|
+
// v0.36.4: workspace rows open the same empty reorder slot a group
|
|
6110
|
+
// row does (the official 12px insert line alone read as "没撑开").
|
|
6111
|
+
// The half split is judged in LOGICAL height, so the grown row
|
|
6112
|
+
// cannot flip its own band under the pointer.
|
|
6113
|
+
className: cls('bw-row', 'bw-container-row', currentInside && 'bw-row-current', menuOpen && 'menu-open', dropHalf === 'before' && 'bw-slot-above', dropHalf === 'after' && 'bw-slot-below', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6058
6114
|
'data-row-key': dataRowKey,
|
|
6059
6115
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
6060
6116
|
onClick: onToggle,
|
|
@@ -6071,14 +6127,10 @@ window.__ModuleLoader__.load({
|
|
|
6071
6127
|
E('span', { className: cls('bw-row-icon', 'bw-folder-cell', currentInside && !pulse && 'bw-folder-active') }, iconChild),
|
|
6072
6128
|
E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text'), title: workspace.title || workspace.leaf }, workspace.leaf),
|
|
6073
6129
|
count > 0 ? E('span', { className: 'bw-row-count' }, String(count)) : null,
|
|
6130
|
+
// The in-row ellipsis is retired (mirrors the session-row decision):
|
|
6131
|
+
// the ctx surface is the right-click path; hover keeps only the
|
|
6132
|
+
// new-session affordance.
|
|
6074
6133
|
E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
|
|
6075
|
-
E('button', {
|
|
6076
|
-
type: 'button',
|
|
6077
|
-
className: 'bw-row-btn',
|
|
6078
|
-
'aria-label': menuAria || '...',
|
|
6079
|
-
title: menuAria || undefined,
|
|
6080
|
-
onClick: (e) => { e.stopPropagation(); if (onMenu) onMenu(e) },
|
|
6081
|
-
}, icon('IconEllipsisOutline16', 15)),
|
|
6082
6134
|
E('button', {
|
|
6083
6135
|
type: 'button',
|
|
6084
6136
|
className: 'bw-row-btn',
|
|
@@ -6487,7 +6539,7 @@ window.__ModuleLoader__.load({
|
|
|
6487
6539
|
'IconSkillOutline16', 'IconSparkle16', 'IconNewChatOutline16',
|
|
6488
6540
|
'IconCheckOutline14', 'IconCheckOutline16', 'IconChevronDownOutline14', 'IconChevronLeftOutline14',
|
|
6489
6541
|
'IconChevronRightOutline14', 'IconChevronUpOutline14', 'IconCloseOutline16', 'IconDarkOutline16',
|
|
6490
|
-
'IconEditOutline16', '
|
|
6542
|
+
'IconEditOutline16', 'IconFolderClose16', 'IconFolderOpen16',
|
|
6491
6543
|
'IconFolderOpenOutline16', 'IconFullscreenOutline16', 'IconLightOutline16', 'IconLinkOutline14',
|
|
6492
6544
|
'IconLoadingOutline16', 'IconPanelLeftOutline16', 'IconPaperclipOutline16', 'IconPersonalizationOutline16',
|
|
6493
6545
|
'IconPlayOutline16', 'IconPauseOutline16', 'IconPlusOutline16', 'IconQuestionOutline14',
|
|
@@ -6946,7 +6998,6 @@ window.__ModuleLoader__.load({
|
|
|
6946
6998
|
const localStyling = useStore ? (useStore(s => s.styling) || {}) : {}
|
|
6947
6999
|
const compactChains = prefs.compactChains !== false
|
|
6948
7000
|
const statusPulse = prefs.statusPulse !== false
|
|
6949
|
-
const sessionMenu = prefs.sessionMenu !== false
|
|
6950
7001
|
const rowActions = prefs.rowActions !== false
|
|
6951
7002
|
// v0.30.2: auto titles keep the '/' group prefix — on unless opted out.
|
|
6952
7003
|
const graftPrefix = prefs.graftTitlePrefix !== false
|
|
@@ -7093,18 +7144,6 @@ window.__ModuleLoader__.load({
|
|
|
7093
7144
|
E('div', { className: 'bw-hint' }, t('settings.sessionLimit.hint')),
|
|
7094
7145
|
),
|
|
7095
7146
|
fold('settings.group.interaction',
|
|
7096
|
-
E('div', { className: 'bw-setting-row' },
|
|
7097
|
-
E('div', { className: 'bw-setting-label' }, t('settings.sessionMenu')),
|
|
7098
|
-
E('button', {
|
|
7099
|
-
type: 'button',
|
|
7100
|
-
role: 'switch',
|
|
7101
|
-
'aria-checked': sessionMenu,
|
|
7102
|
-
'aria-label': t('settings.sessionMenu'),
|
|
7103
|
-
className: cls('bw-switch', sessionMenu && 'bw-switch-on'),
|
|
7104
|
-
onClick: () => { setPref('sessionMenu', !sessionMenu) },
|
|
7105
|
-
}, E('span', { className: 'bw-switch-thumb' })),
|
|
7106
|
-
),
|
|
7107
|
-
E('div', { className: 'bw-hint' }, t('settings.sessionMenu.hint')),
|
|
7108
7147
|
E('div', { className: 'bw-setting-row' },
|
|
7109
7148
|
E('div', { className: 'bw-setting-label' }, t('settings.graft.title')),
|
|
7110
7149
|
E('button', {
|
|
@@ -7346,6 +7385,12 @@ window.__ModuleLoader__.load({
|
|
|
7346
7385
|
const wsFolders = typeof useStore === 'function'
|
|
7347
7386
|
? useStore(state => (state && state.folders && state.folders.ws) || EMPTY_FOLDERS)
|
|
7348
7387
|
: EMPTY_FOLDERS
|
|
7388
|
+
// Same persisted group order the sidebar uses (v0.36.4) — a chooser that
|
|
7389
|
+
// listed groups alphabetically while the tree showed the dragged order
|
|
7390
|
+
// would contradict the surface it selects for.
|
|
7391
|
+
const pickerFolderOrder = typeof useStore === 'function'
|
|
7392
|
+
? useStore(state => (state && state.folderOrder) || EMPTY_FOLDERS)
|
|
7393
|
+
: EMPTY_FOLDERS
|
|
7349
7394
|
// Occupancy is the only reactive fact here: the ledger read re-runs on
|
|
7350
7395
|
// every render, so the count just forces the re-render when the shipped
|
|
7351
7396
|
// occupant loads or unloads (its identity is stable while occupied).
|
|
@@ -7367,10 +7412,10 @@ window.__ModuleLoader__.load({
|
|
|
7367
7412
|
// Same construction the sidebar runs (buildTree + optional chain
|
|
7368
7413
|
// compression), so both surfaces answer the same view options.
|
|
7369
7414
|
const tree = React.useMemo(() => {
|
|
7370
|
-
const built = buildTree(items, groupMode, wsFolders)
|
|
7415
|
+
const built = buildTree(items, groupMode, wsFolders, pickerFolderOrder)
|
|
7371
7416
|
if (!compactChains) return built
|
|
7372
7417
|
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
7373
|
-
}, [items, groupMode, wsFolders, compactChains])
|
|
7418
|
+
}, [items, groupMode, wsFolders, pickerFolderOrder, compactChains])
|
|
7374
7419
|
|
|
7375
7420
|
const rows = React.useMemo(() => {
|
|
7376
7421
|
const out = []
|
|
@@ -7601,6 +7646,10 @@ window.__ModuleLoader__.load({
|
|
|
7601
7646
|
const foldersMap = useStore ? (useStore(s => s.folders) || {}) : {}
|
|
7602
7647
|
const wsFolders = foldersMap && typeof foldersMap.ws === 'object' && foldersMap.ws !== null ? foldersMap.ws : EMPTY_FOLDERS
|
|
7603
7648
|
const sessFolders = foldersMap && typeof foldersMap.sess === 'object' && foldersMap.sess !== null ? foldersMap.sess : EMPTY_FOLDERS
|
|
7649
|
+
// Persisted group order (v0.36.4): { [parentKey]: name[] }. Absent on
|
|
7650
|
+
// every snapshot written before it existed — the seed name sort then
|
|
7651
|
+
// stands, so old states render unchanged.
|
|
7652
|
+
const folderOrderMap = useStore ? (useStore(s => s.folderOrder) || EMPTY_FOLDERS) : EMPTY_FOLDERS
|
|
7604
7653
|
// View options (v0.13). Selector reads take fallbacks: hydration
|
|
7605
7654
|
// replaces the whole persisted value, so states written by older
|
|
7606
7655
|
// plugin versions lack these keys (hard store discipline).
|
|
@@ -7629,10 +7678,9 @@ window.__ModuleLoader__.load({
|
|
|
7629
7678
|
const shared = makeSharedWrites(actions, stylingMap)
|
|
7630
7679
|
const compactChains = prefsMap.compactChains !== false
|
|
7631
7680
|
const statusPulse = prefsMap.statusPulse !== false
|
|
7632
|
-
// Session-action surface
|
|
7633
|
-
//
|
|
7634
|
-
//
|
|
7635
|
-
const sessionMenuEnabled = prefsMap.sessionMenu !== false
|
|
7681
|
+
// Session-action surface gate (v0.20.0): the hover quick buttons. The
|
|
7682
|
+
// right-click ctx menu is always on since the in-row "..." triggers
|
|
7683
|
+
// retired (session row v0.35.0, workspace row v0.35.1).
|
|
7636
7684
|
const rowActionsEnabled = prefsMap.rowActions !== false
|
|
7637
7685
|
// Auto color + icon for new items (v0.24.0). Default OFF: absent key =
|
|
7638
7686
|
// off, so every existing snapshot keeps its behavior untouched.
|
|
@@ -7946,13 +7994,22 @@ window.__ModuleLoader__.load({
|
|
|
7946
7994
|
const [copyNotice, setCopyNotice] = React.useState(null) // { seq, ok } copy-session-id notice
|
|
7947
7995
|
const [customize, setCustomize] = React.useState(null) // { kind, entryKey, name }
|
|
7948
7996
|
const [errorText, setErrorText] = React.useState(null)
|
|
7949
|
-
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session', source, over } | null
|
|
7997
|
+
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session'|'folder', source, over } | null
|
|
7950
7998
|
// Workspace drags arm their state one frame LATE (see workspaceDragEvents):
|
|
7951
7999
|
// arming synchronously re-renders during the dragstart dispatch, the chain
|
|
7952
8000
|
// expansion inserts rows above the drag source, the cursor leaves the
|
|
7953
8001
|
// source element, and Chromium cancels the whole gesture. dragEnd clears
|
|
7954
8002
|
// the timer so a same-tick cancel never leaves a ghost drag behind.
|
|
7955
8003
|
const wsDragArmTimer = React.useRef(null)
|
|
8004
|
+
// (v0.36.3) The 600ms group-hover auto-expand is retired: it kept
|
|
8005
|
+
// opening groups under a passing cursor ("变下级会被撑开"). The
|
|
8006
|
+
// deterministic reveal now lives in commitFolderMoveInto — the target
|
|
8007
|
+
// group opens when the members actually land.
|
|
8008
|
+
// (v0.36.4) The hover-intent arming is retired. It made the reorder
|
|
8009
|
+
// slot appear only after a ~200ms rest on the band, so a normal drag
|
|
8010
|
+
// across a group row showed the thin insert line and never the empty
|
|
8011
|
+
// slot — the user report "还是一条蓝线,没拨开让我插入". The edge bands
|
|
8012
|
+
// are 30% wide and geometrically judged, so they can answer at once.
|
|
7956
8013
|
// Quote-on-land eligibility sets (mount-scoped):
|
|
7957
8014
|
// - blankSeen: ids observed BLANK in any snapshot. A blank row is a
|
|
7958
8015
|
// freshly created New Session, so ONLY these may ever receive an
|
|
@@ -8109,7 +8166,12 @@ window.__ModuleLoader__.load({
|
|
|
8109
8166
|
const normalizedQuery = query.trim().toLowerCase()
|
|
8110
8167
|
const now = Date.now()
|
|
8111
8168
|
|
|
8112
|
-
|
|
8169
|
+
// v0.36.4: no setFlowOpen here. flowOpen belongs to the PICKER component
|
|
8170
|
+
// (WorkspacePicker), not to this browser: referencing it from here threw
|
|
8171
|
+
// 'ReferenceError: setFlowOpen is not defined' on every failure path —
|
|
8172
|
+
// including the .catch(fail) of a group move-into, which then silently
|
|
8173
|
+
// aborted the rename ('变下级没反应').
|
|
8174
|
+
const fail = (text) => { setDialog(null); setErrorText(String(text || 'unknown error')) }
|
|
8113
8175
|
|
|
8114
8176
|
const styleEntry = (key) => stylingMap[key] || null
|
|
8115
8177
|
// Effective appearance = the settings card's default, overridden field by
|
|
@@ -8138,7 +8200,11 @@ window.__ModuleLoader__.load({
|
|
|
8138
8200
|
return Object.keys(style).length > 0 ? style : null
|
|
8139
8201
|
}
|
|
8140
8202
|
const keyOf = (kind, payload) => {
|
|
8141
|
-
|
|
8203
|
+
// Render keys folders by idPath (renderPlainFolder), which prefixes
|
|
8204
|
+
// the ws id to disambiguate same-named groups across disk levels;
|
|
8205
|
+
// saving by payload.path alone made sub-level group styling
|
|
8206
|
+
// unreachable (name-slash group appearance never applied).
|
|
8207
|
+
if (kind === 'folder') return 'folder:' + (payload.idPath || payload.path)
|
|
8142
8208
|
if (kind === 'workspace') return 'workspace:' + payload.workspaceId
|
|
8143
8209
|
if (kind === 'session') return 'session:' + payload.id
|
|
8144
8210
|
if (kind === 'sgroup') return 'sgroup:' + payload.workspaceId + '|' + payload.path
|
|
@@ -8172,26 +8238,33 @@ window.__ModuleLoader__.load({
|
|
|
8172
8238
|
pinnedPartitionInPlace(ungrouped)
|
|
8173
8239
|
}
|
|
8174
8240
|
|
|
8175
|
-
//
|
|
8176
|
-
// a
|
|
8177
|
-
//
|
|
8178
|
-
//
|
|
8179
|
-
//
|
|
8180
|
-
//
|
|
8181
|
-
// their drop targets live inside workspace rows, which compression merges.
|
|
8182
|
-
const draggingWorkspace = drag !== null && drag.kind === 'workspace'
|
|
8241
|
+
// (v0.36.4) The drag-time chain EXPANSION is retired. Suspending the
|
|
8242
|
+
// merge while a workspace was dragged re-exposed every single-child
|
|
8243
|
+
// chain as folder rows, so the tree visibly re-laid itself one frame
|
|
8244
|
+
// after dragstart and every drop target below the source shifted —
|
|
8245
|
+
// the user report "拖拽中树会乱/落点漂移". Drop targets are now
|
|
8246
|
+
// answered by the stable render; nothing re-flows mid-drag.
|
|
8183
8247
|
const tree = React.useMemo(() => {
|
|
8184
|
-
const built = buildTree(items, workspaceGroupMode, wsFolders)
|
|
8185
|
-
if (!compactChains
|
|
8186
|
-
|
|
8187
|
-
|
|
8248
|
+
const built = buildTree(items, workspaceGroupMode, wsFolders, folderOrderMap)
|
|
8249
|
+
if (!compactChains) return built
|
|
8250
|
+
const folders = built.folders.map((f) => materializeChain(compressTree(f)))
|
|
8251
|
+
// v0.36.5: keep the MIXED root sequence across chain compression —
|
|
8252
|
+
// the compressed node stands in for the raw folder it came from, so
|
|
8253
|
+
// a bare workspace stays interleaved where the user put it.
|
|
8254
|
+
const byName = new Map()
|
|
8255
|
+
for (let i = 0; i < built.folders.length; i += 1) byName.set(built.folders[i].name, folders[i])
|
|
8256
|
+
const seq = Array.isArray(built.seq)
|
|
8257
|
+
? built.seq.map((item) => item.kind === 'folder' ? { kind: 'folder', node: byName.get(item.node.name) || item.node } : item)
|
|
8258
|
+
: built.seq
|
|
8259
|
+
return { ...built, folders, workspaces: built.workspaces, seq }
|
|
8260
|
+
}, [items, compactChains, workspaceGroupMode, wsFolders, folderOrderMap])
|
|
8188
8261
|
// UNCOMPRESSED tree, for the new-group dialog only. compressTree merges a
|
|
8189
8262
|
// single-child chain into one row that stands for BOTH a group and the
|
|
8190
8263
|
// workspace under it — fine for drawing, useless for choosing a
|
|
8191
8264
|
// container: one half must stay unselectable and the other must be
|
|
8192
8265
|
// selectable, and a merged row cannot be both. (It also carried no
|
|
8193
8266
|
// `name`, which is how a lone "E" rendered blank.)
|
|
8194
|
-
const rawTree = React.useMemo(() => buildTree(items, workspaceGroupMode, wsFolders), [items, workspaceGroupMode, wsFolders])
|
|
8267
|
+
const rawTree = React.useMemo(() => buildTree(items, workspaceGroupMode, wsFolders, folderOrderMap), [items, workspaceGroupMode, wsFolders, folderOrderMap])
|
|
8195
8268
|
// Disk-layer ancestry for drag semantics (same derivation as buildTree).
|
|
8196
8269
|
const diskParentMap = React.useMemo(() => diskParentMapOf(items), [items])
|
|
8197
8270
|
|
|
@@ -8391,9 +8464,8 @@ window.__ModuleLoader__.load({
|
|
|
8391
8464
|
const unarchiveAvailable = typeof unarchiveSession === 'function'
|
|
8392
8465
|
const sessionActions = {
|
|
8393
8466
|
ot: officialT,
|
|
8394
|
-
// Right-click is the
|
|
8395
|
-
//
|
|
8396
|
-
trigger: !sessionMenuEnabled,
|
|
8467
|
+
// Right-click is the only ctx trigger: the in-row "..." affordances
|
|
8468
|
+
// are retired (session row in v0.35.0, workspace row likewise).
|
|
8397
8469
|
quick: rowActionsEnabled,
|
|
8398
8470
|
pin: pinAvailable
|
|
8399
8471
|
? (id, pinning) => {
|
|
@@ -8695,11 +8767,72 @@ window.__ModuleLoader__.load({
|
|
|
8695
8767
|
}
|
|
8696
8768
|
}, [drag === null])
|
|
8697
8769
|
|
|
8770
|
+
// Geometry helpers shared by both reorder targets. v0.36.4 judges in
|
|
8771
|
+
// v0.36.4 geometry: an open reorder slot (bw-slot-above / -below) pads
|
|
8772
|
+
// the row open by one logical row (padding, so the box grows to two
|
|
8773
|
+
// rows). Judge the pointer against the row's ORIGINAL position, not the
|
|
8774
|
+
// padded box: the original position is the half its content did NOT
|
|
8775
|
+
// slide into — the pad above for a before-slot, the remaining content
|
|
8776
|
+
// box for an after-slot. Judging there keeps a still pointer from
|
|
8777
|
+
// flipping the slot the moment it opens (the reported "抽搐"), while
|
|
8778
|
+
// still letting the pointer cross the original midpoint to switch
|
|
8779
|
+
// direction — the padded half answers null instead of a stale band.
|
|
8780
|
+
const originalBoxOf = (el) => {
|
|
8781
|
+
const rect = el.getBoundingClientRect()
|
|
8782
|
+
const style = getComputedStyle(el)
|
|
8783
|
+
const padTop = parseFloat(style.paddingTop) || 0
|
|
8784
|
+
const padBottom = parseFloat(style.paddingBottom) || 0
|
|
8785
|
+
if (padTop > 0) return { top: rect.top, bottom: rect.top + padTop }
|
|
8786
|
+
if (padBottom > 0) return { top: rect.top, bottom: rect.bottom - padBottom }
|
|
8787
|
+
return { top: rect.top, bottom: rect.bottom }
|
|
8788
|
+
}
|
|
8789
|
+
// null = the pointer sits in the half the open slot vacated: the caller
|
|
8790
|
+
// keeps its current drop target instead of re-deciding on geometry that
|
|
8791
|
+
// only exists because of that slot.
|
|
8792
|
+
// v0.36.5 MIXED LEVEL SEQUENCE. buildTree attaches a `seq` to every
|
|
8793
|
+
// level (group rows and BARE workspace rows interleaved by the
|
|
8794
|
+
// persisted folderOrder, whose tokens may be a group name or
|
|
8795
|
+
// "w:<workspaceId>"); chain-compressed nodes carry no seq and keep the
|
|
8796
|
+
// historical folders-then-workspaces order. Both the renderer and the
|
|
8797
|
+
// reorder commit read the SAME sequence, so what the user sees is what
|
|
8798
|
+
// the drop edits.
|
|
8799
|
+
const levelSeqOf = (level) => (level && Array.isArray(level.seq))
|
|
8800
|
+
? level.seq
|
|
8801
|
+
: [
|
|
8802
|
+
...(((level && level.folders) || []).map((node) => ({ kind: 'folder', node }))),
|
|
8803
|
+
...(((level && level.workspaces) || []).map((entry) => ({ kind: 'ws', entry }))),
|
|
8804
|
+
]
|
|
8805
|
+
const levelTokenOf = (item) => item.kind === 'ws' ? 'w:' + item.entry.workspaceId : item.node.name
|
|
8698
8806
|
const rowHalf = (event) => {
|
|
8699
|
-
const
|
|
8700
|
-
|
|
8807
|
+
const box = originalBoxOf(event.currentTarget)
|
|
8808
|
+
const y = event.clientY
|
|
8809
|
+
if (y < box.top || y > box.bottom) return null
|
|
8810
|
+
return y < (box.top + box.bottom) / 2 ? 'before' : 'after'
|
|
8811
|
+
}
|
|
8812
|
+
// Three-band targeting for group rows (react-arborist style): the
|
|
8813
|
+
// outer 30% of the CONTENT means "reorder against this group", the
|
|
8814
|
+
// middle 40% means "drop into" — so move-into (the framed highlight)
|
|
8815
|
+
// is easy to hit and reorder never swallows the pointer. v0.36.3
|
|
8816
|
+
// widened the bands from a quarter: at 25% they were 8.5px on a 34px
|
|
8817
|
+
// row and practically unhittable (every drop landed "into" instead
|
|
8818
|
+
// of reordering beside).
|
|
8819
|
+
const rowZone = (event) => {
|
|
8820
|
+
const box = originalBoxOf(event.currentTarget)
|
|
8821
|
+
const y = event.clientY
|
|
8822
|
+
if (y < box.top || y > box.bottom) return null
|
|
8823
|
+
const h = box.bottom - box.top
|
|
8824
|
+
if (h < 2) return 'into'
|
|
8825
|
+
const rel = (y - box.top) / h
|
|
8826
|
+
if (rel < 0.3) return 'before'
|
|
8827
|
+
if (rel > 0.7) return 'after'
|
|
8828
|
+
return 'into'
|
|
8701
8829
|
}
|
|
8702
8830
|
const dragMatches = (kind) => drag !== null && drag.kind === kind
|
|
8831
|
+
// A refused hover must DROP the highlight it already owns: leaving a
|
|
8832
|
+
// stale {over} in place drew the previous target's INTO frame on a row
|
|
8833
|
+
// that no longer accepts anything (the user reads it as "it will land
|
|
8834
|
+
// here" and the drop then goes elsewhere).
|
|
8835
|
+
const clearOver = (targetKind, target) => setDrag((current) => (current && current.over && current.over.kind === targetKind && current.over.target === target) ? { ...current, over: null } : current)
|
|
8703
8836
|
const canDragWorkspace = typeof insertWorkspaceBefore === 'function'
|
|
8704
8837
|
// Session REORDER needs the host insertSessionBefore action; dragging a
|
|
8705
8838
|
// session onto a sub-group row (a pure rename) stays available without it.
|
|
@@ -8711,12 +8844,26 @@ window.__ModuleLoader__.load({
|
|
|
8711
8844
|
|
|
8712
8845
|
/* --------------------- workspace drag & drop -------------------- */
|
|
8713
8846
|
|
|
8847
|
+
// v0.36.5: a workspace row answers the SAME slot for both drag kinds —
|
|
8848
|
+
// a workspace slides between workspace rows, a whole group block slides
|
|
8849
|
+
// beside one (mixed reorder), so the visual is one shared mechanism.
|
|
8714
8850
|
const wsDropHalf = (workspaceId) => {
|
|
8715
|
-
if (!dragMatches('workspace')) return null
|
|
8851
|
+
if (!dragMatches('workspace') && !dragMatches('folder')) return null
|
|
8716
8852
|
const over = drag.over
|
|
8717
8853
|
return over && over.kind === 'workspace' && over.target === workspaceId ? over.half : null
|
|
8718
8854
|
}
|
|
8719
|
-
|
|
8855
|
+
// Folder rows light up for a workspace drop (regroup one workspace)
|
|
8856
|
+
// AND a folder drop (whole-group move) alike — the INTO band only:
|
|
8857
|
+
// an edge-band reorder slot draws the official insert line instead.
|
|
8858
|
+
const wsDropInto = (path) => (dragMatches('workspace') || dragMatches('folder')) && drag.over && drag.over.kind === 'folder' && drag.over.target === path && (!drag.over.zone || drag.over.zone === 'into')
|
|
8859
|
+
// v0.36.4: an edge band on a group row answers IMMEDIATELY with the
|
|
8860
|
+
// empty reorder slot (bw-slot-above/below) — the drop position is
|
|
8861
|
+
// visible the moment the pointer enters the band, no intent wait.
|
|
8862
|
+
// v0.36.5: BOTH sources open the slot on a group row. A folder source
|
|
8863
|
+
// reorders the group block; a WORKSPACE source interleaves one bare
|
|
8864
|
+
// workspace row beside the group — the gesture "insert as the first
|
|
8865
|
+
// row" that used to be impossible because every group rendered first.
|
|
8866
|
+
const wsFolderSlotOf = (folderIdPath) => ((dragMatches('folder') || dragMatches('workspace')) && drag.over && drag.over.kind === 'folder' && drag.over.target === folderIdPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) ? drag.over.zone : null
|
|
8720
8867
|
const workspaceDragEvents = (workspace) => ({
|
|
8721
8868
|
draggable: !searching && canDragWorkspace,
|
|
8722
8869
|
onDragStart: (event) => {
|
|
@@ -8745,7 +8892,7 @@ window.__ModuleLoader__.load({
|
|
|
8745
8892
|
if (wsDragArmTimer.current !== null) clearTimeout(wsDragArmTimer.current)
|
|
8746
8893
|
wsDragArmTimer.current = setTimeout(() => {
|
|
8747
8894
|
wsDragArmTimer.current = null
|
|
8748
|
-
setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: sourceLeaf, folderPath: sourceFolder, disk: diskParentMap.get(workspace.workspaceId) }, over: null })
|
|
8895
|
+
setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: sourceLeaf, folderPath: sourceFolder, disk: diskParentMap.get(workspace.workspaceId), levelKey: workspace.levelKey || '' }, over: null })
|
|
8749
8896
|
}, 0)
|
|
8750
8897
|
},
|
|
8751
8898
|
onDragEnd: () => {
|
|
@@ -8769,11 +8916,57 @@ window.__ModuleLoader__.load({
|
|
|
8769
8916
|
: (current ? { ...current, over: { kind: 'ws-root', target: workspace.workspaceId } } : current))
|
|
8770
8917
|
return
|
|
8771
8918
|
}
|
|
8919
|
+
// v0.36.5 MIXED REORDER: a GROUP block can slide beside a bare
|
|
8920
|
+
// workspace row of the same level (the mirror gesture of a
|
|
8921
|
+
// workspace sliding beside a group row). Siblings only — a group
|
|
8922
|
+
// that lives in another container has no position in this level.
|
|
8923
|
+
if (dragMatches('folder')) {
|
|
8924
|
+
const sourceLevel = drag.source.levelKey || ''
|
|
8925
|
+
if (sourceLevel !== (workspace.levelKey || '')) {
|
|
8926
|
+
// v0.36.7 INTO on the OWNING disk row: a group block dropped on
|
|
8927
|
+
// the workspace row that CONTAINS it answers the same framed
|
|
8928
|
+
// INTO a workspace row gets on a group row ("放入这个工作区
|
|
8929
|
+
// 下"). The only position that means without rewriting a path
|
|
8930
|
+
// is the tail of the level it already lives in, so the drop
|
|
8931
|
+
// commits moveFolderToLevelEnd. Every other cross-level target
|
|
8932
|
+
// stays refused: the disk layer is a filesystem fact.
|
|
8933
|
+
if (sourceLevel === (workspace.subPrefix || '')) {
|
|
8934
|
+
event.preventDefault()
|
|
8935
|
+
event.stopPropagation()
|
|
8936
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8937
|
+
setDrag(current => (current && current.over && current.over.kind === 'ws-level' && current.over.target === workspace.workspaceId)
|
|
8938
|
+
? current
|
|
8939
|
+
: (current ? { ...current, over: { kind: 'ws-level', target: workspace.workspaceId } } : current))
|
|
8940
|
+
} else {
|
|
8941
|
+
clearOver('workspace', workspace.workspaceId)
|
|
8942
|
+
clearOver('ws-level', workspace.workspaceId)
|
|
8943
|
+
}
|
|
8944
|
+
return
|
|
8945
|
+
}
|
|
8946
|
+
event.preventDefault()
|
|
8947
|
+
event.stopPropagation()
|
|
8948
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8949
|
+
const half = rowHalf(event)
|
|
8950
|
+
if (half === null) return
|
|
8951
|
+
setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
|
|
8952
|
+
? current
|
|
8953
|
+
: (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
|
|
8954
|
+
return
|
|
8955
|
+
}
|
|
8772
8956
|
if (!dragMatches('workspace')) return
|
|
8957
|
+
// v0.36.4: the SOURCE row is never its own drop target. A reorder
|
|
8958
|
+
// slot pads a row open by one logical row, so the row the drag
|
|
8959
|
+
// started on ends up covering the pointer's first stop — without
|
|
8960
|
+
// this gate the source row swallowed the target, the drop landed on
|
|
8961
|
+
// itself, commitWorkspaceDrop no-oped and the gesture read as
|
|
8962
|
+
// "拖了没反应".
|
|
8963
|
+
if (drag.source && drag.source.workspaceId === workspace.workspaceId) return
|
|
8773
8964
|
event.preventDefault()
|
|
8774
8965
|
event.stopPropagation()
|
|
8775
8966
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8776
8967
|
const half = rowHalf(event)
|
|
8968
|
+
// Pointer in the half the open slot vacated: keep this row's slot.
|
|
8969
|
+
if (half === null) return
|
|
8777
8970
|
setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
|
|
8778
8971
|
? current
|
|
8779
8972
|
: (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
|
|
@@ -8787,6 +8980,24 @@ window.__ModuleLoader__.load({
|
|
|
8787
8980
|
commitSessionMoveOut(workspace.workspaceId)
|
|
8788
8981
|
return
|
|
8789
8982
|
}
|
|
8983
|
+
if (dragMatches('folder')) {
|
|
8984
|
+
const sourceLevel = drag.source.levelKey || ''
|
|
8985
|
+
if (sourceLevel !== (workspace.levelKey || '')) {
|
|
8986
|
+
// v0.36.7: the group's OWNING disk row accepts the framed INTO
|
|
8987
|
+
// (see onDragOver) — put the block back at that level's tail.
|
|
8988
|
+
if (sourceLevel === (workspace.subPrefix || '')) {
|
|
8989
|
+
event.preventDefault()
|
|
8990
|
+
event.stopPropagation()
|
|
8991
|
+
commitFolderMoveToLevelEnd(drag.source)
|
|
8992
|
+
}
|
|
8993
|
+
return
|
|
8994
|
+
}
|
|
8995
|
+
event.preventDefault()
|
|
8996
|
+
event.stopPropagation()
|
|
8997
|
+
const half = drag.over && drag.over.kind === 'workspace' && drag.over.target === workspace.workspaceId ? drag.over.half : rowHalf(event)
|
|
8998
|
+
commitLevelReorder({ kind: 'folder', source: drag.source }, { kind: 'workspace', workspace }, half || 'before')
|
|
8999
|
+
return
|
|
9000
|
+
}
|
|
8790
9001
|
if (!dragMatches('workspace')) return
|
|
8791
9002
|
event.preventDefault()
|
|
8792
9003
|
event.stopPropagation()
|
|
@@ -8794,21 +9005,214 @@ window.__ModuleLoader__.load({
|
|
|
8794
9005
|
commitWorkspaceDrop(workspace, half)
|
|
8795
9006
|
},
|
|
8796
9007
|
})
|
|
8797
|
-
|
|
9008
|
+
// A folder row accepts BOTH drag kinds: a workspace drop regroups that
|
|
9009
|
+
// one workspace (commitWorkspaceMoveInto), a folder drop moves the
|
|
9010
|
+
// whole group — every member retitled under the target prefix
|
|
9011
|
+
// (commitFolderMoveInto, the drag twin of submitFolderRename).
|
|
9012
|
+
// Dropping a group onto itself or its own descendant would loop, so
|
|
9013
|
+
// the hover refuses there and the browser shows its "no drop" cursor.
|
|
9014
|
+
const namePathOf = (folderIdPath) => {
|
|
9015
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9016
|
+
return cutAt === -1 ? folderIdPath : folderIdPath.slice(cutAt + 2)
|
|
9017
|
+
}
|
|
9018
|
+
// idPath = [<wsId>//]*<namePath>: the owner prefix names the disk
|
|
9019
|
+
// level a group renders in. Reordering only makes sense between
|
|
9020
|
+
// groups of the SAME level — across levels a group's position
|
|
9021
|
+
// belongs to its own container.
|
|
9022
|
+
const ownerPrefixOf = (folderIdPath) => {
|
|
9023
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9024
|
+
return cutAt === -1 ? '' : folderIdPath.slice(0, cutAt)
|
|
9025
|
+
}
|
|
9026
|
+
// PARENT container identity of a group row (v0.36.4) — the key its
|
|
9027
|
+
// sibling order is persisted under. The ROOT level of a disk scope
|
|
9028
|
+
// answers the scope prefix ('' at the top level, '<wsId>' one disk
|
|
9029
|
+
// level down); a deeper group answers the idPath of its parent group.
|
|
9030
|
+
const parentKeyOf = (folderIdPath) => {
|
|
9031
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9032
|
+
const prefix = cutAt === -1 ? '' : folderIdPath.slice(0, cutAt)
|
|
9033
|
+
const namePath = cutAt === -1 ? folderIdPath : folderIdPath.slice(cutAt + 2)
|
|
9034
|
+
const segs = namePath.split('/')
|
|
9035
|
+
segs.pop()
|
|
9036
|
+
const parentNamePath = segs.join('/')
|
|
9037
|
+
return parentNamePath === '' ? prefix : prefix + '//' + parentNamePath
|
|
9038
|
+
}
|
|
9039
|
+
// Locate the container a group order list belongs to inside the RAW
|
|
9040
|
+
// (uncompressed) tree: the ROOT level, a disk sub-level (a
|
|
9041
|
+
// workspace's `sub`), or a group node whose `folders` are its
|
|
9042
|
+
// children. Compression never runs on rawTree, so every group the
|
|
9043
|
+
// user can drop beside still has its own idPath here.
|
|
9044
|
+
const folderContainerOf = (key) => {
|
|
9045
|
+
if (key === '') return rawTree
|
|
9046
|
+
let hit = null
|
|
9047
|
+
const walk = (level) => {
|
|
9048
|
+
if (hit || !level) return
|
|
9049
|
+
for (const w of level.workspaces || []) {
|
|
9050
|
+
if (hit) return
|
|
9051
|
+
if (w.subPrefix === key && w.sub) { hit = w.sub; return }
|
|
9052
|
+
if (w.sub) walk(w.sub)
|
|
9053
|
+
}
|
|
9054
|
+
for (const f of level.folders || []) {
|
|
9055
|
+
if (hit) return
|
|
9056
|
+
if (f.idPath === key) { hit = f; return }
|
|
9057
|
+
walk(f)
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
walk(rawTree)
|
|
9061
|
+
return hit
|
|
9062
|
+
}
|
|
9063
|
+
// Whole-block member ids of a name group, from this rendering level
|
|
9064
|
+
// down: a reorder slides EVERY nested workspace (prefixes untouched,
|
|
9065
|
+
// so sub-groups travel with the block; only the registry order
|
|
9066
|
+
// shifts).
|
|
9067
|
+
const collectMemberIds = (folderNode) => {
|
|
9068
|
+
const acc = []
|
|
9069
|
+
const walk = (level) => {
|
|
9070
|
+
for (const w of level.workspaces || []) acc.push(w.workspaceId)
|
|
9071
|
+
for (const f of level.folders || []) {
|
|
9072
|
+
if (f.kind === 'ws') acc.push(f.workspace.workspaceId)
|
|
9073
|
+
else walk(f)
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
walk(folderNode)
|
|
9077
|
+
return acc
|
|
9078
|
+
}
|
|
9079
|
+
const folderDropEvents = (idPath, path, node) => ({
|
|
9080
|
+
draggable: !searching,
|
|
9081
|
+
onDragStart: (event) => {
|
|
9082
|
+
if (searching) return
|
|
9083
|
+
event.stopPropagation()
|
|
9084
|
+
try {
|
|
9085
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
9086
|
+
event.dataTransfer.setData('text/plain', idPath)
|
|
9087
|
+
} catch { /* drag payload is best-effort */ }
|
|
9088
|
+
// Same one-frame arm as the workspace drag: Chromium cancels a
|
|
9089
|
+
// just-started drag whose source element moves under the cursor.
|
|
9090
|
+
if (wsDragArmTimer.current !== null) clearTimeout(wsDragArmTimer.current)
|
|
9091
|
+
wsDragArmTimer.current = setTimeout(() => {
|
|
9092
|
+
wsDragArmTimer.current = null
|
|
9093
|
+
setDrag({ kind: 'folder', source: { idPath, path, memberIds: collectMemberIds(node), levelKey: parentKeyOf(idPath) }, over: null })
|
|
9094
|
+
}, 0)
|
|
9095
|
+
},
|
|
9096
|
+
onDragEnd: () => {
|
|
9097
|
+
if (wsDragArmTimer.current !== null) { clearTimeout(wsDragArmTimer.current); wsDragArmTimer.current = null }
|
|
9098
|
+
setDrag(null)
|
|
9099
|
+
},
|
|
8798
9100
|
onDragOver: (event) => {
|
|
8799
|
-
|
|
9101
|
+
// Three bands on a group row: the outer 30% bands reorder this
|
|
9102
|
+
// group among its SIBLINGS (an empty slot opens at once — the
|
|
9103
|
+
// drop position is visible the moment the band is entered); the
|
|
9104
|
+
// middle 40% moves the source INSIDE the hovered group (framed,
|
|
9105
|
+
// never opened). Workspace sources keep whole-row move-into
|
|
9106
|
+
// semantics.
|
|
9107
|
+
//
|
|
9108
|
+
// SIBLINGS means the SAME PARENT (parentKeyOf): a band reorder
|
|
9109
|
+
// only permutes one level's group name list, it never rewrites a
|
|
9110
|
+
// title prefix. Cross-parent and self/descendant targets are
|
|
9111
|
+
// refused — no preventDefault, so the platform shows its own
|
|
9112
|
+
// 'no drop' cursor, which is the truth.
|
|
9113
|
+
//
|
|
9114
|
+
// v0.36.4: once THIS row already holds a reorder slot, the whole
|
|
9115
|
+
// original row position answers before/after ONLY (a plain
|
|
9116
|
+
// midpoint split). Judging the three bands again there reintroduced
|
|
9117
|
+
// the INTO band on the very row that just padded open: a pointer
|
|
9118
|
+
// drifting a few pixels inside it flipped to INTO, closed the slot
|
|
9119
|
+
// and read as a flicker ("抽搐") — and worse, the drop then landed
|
|
9120
|
+
// as move-into (a no-op) instead of the reorder the user aimed at.
|
|
9121
|
+
// The three bands still decide the FIRST entry, so INTO (move the
|
|
9122
|
+
// source inside this group) stays reachable.
|
|
9123
|
+
const slotOpen = drag.over !== null && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')
|
|
9124
|
+
const zone = slotOpen ? rowHalf(event) : rowZone(event)
|
|
9125
|
+
const isFolder = dragMatches('folder')
|
|
9126
|
+
const isWorkspace = dragMatches('workspace')
|
|
9127
|
+
if (isFolder) {
|
|
9128
|
+
const targetPath = namePathOf(idPath)
|
|
9129
|
+
// Self / descendant: looping.
|
|
9130
|
+
if (drag.source.path === targetPath || targetPath.startsWith(drag.source.path + '/')) { clearOver('folder', idPath); return }
|
|
9131
|
+
// Ancestor: the group already lives inside. The DIRECT parent is
|
|
9132
|
+
// the container it lives in, so v0.36.7 reopens its MIDDLE band
|
|
9133
|
+
// (the framed INTO): "放进那个组" can only mean "move to the end
|
|
9134
|
+
// of that group", the same feedback a workspace row gets when
|
|
9135
|
+
// dropped INTO its own group. Edge bands still mean nothing
|
|
9136
|
+
// there (leaving a parent is a title rename, not an order
|
|
9137
|
+
// change), and a further ancestor stays refused.
|
|
9138
|
+
if (targetPath !== '' && drag.source.path.startsWith(targetPath + '/')) {
|
|
9139
|
+
const cutAt = drag.source.path.lastIndexOf('/')
|
|
9140
|
+
const directParent = cutAt === -1 ? '' : drag.source.path.slice(0, cutAt)
|
|
9141
|
+
if (targetPath !== directParent || zone !== 'into') { clearOver('folder', idPath); return }
|
|
9142
|
+
}
|
|
9143
|
+
// Pointer in the half the open slot vacated: keep this row's slot
|
|
9144
|
+
// (re-deciding there would flicker the slot it just opened).
|
|
9145
|
+
if (zone === null) {
|
|
9146
|
+
if (drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) {
|
|
9147
|
+
event.preventDefault()
|
|
9148
|
+
event.stopPropagation()
|
|
9149
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
9150
|
+
}
|
|
9151
|
+
return
|
|
9152
|
+
}
|
|
9153
|
+
// Edge bands permute ONE level's sibling list; a cross-parent
|
|
9154
|
+
// band is refused. Move-into (the middle band) stays open across
|
|
9155
|
+
// parents: it is a batch rename, which does reach another group.
|
|
9156
|
+
if (zone !== 'into' && parentKeyOf(idPath) !== parentKeyOf(drag.source.idPath)) { clearOver('folder', idPath); return }
|
|
9157
|
+
} else if (!isWorkspace) {
|
|
9158
|
+
return
|
|
9159
|
+
} else {
|
|
9160
|
+
// v0.36.6 LIFT: a bare workspace row slides beside a group of ANY
|
|
9161
|
+
// level it can reach. Same level keeps the v0.36.5 mixed reorder.
|
|
9162
|
+
// Another level is a LIFT: the workspace leaves its current group
|
|
9163
|
+
// and joins the target group's level, inserted right there. That
|
|
9164
|
+
// is the "insert as the FIRST row" gesture the user kept
|
|
9165
|
+
// reporting: the hover used to be refused silently, so {over}
|
|
9166
|
+
// kept the previous frame's INTO, and the drop became a
|
|
9167
|
+
// move-into that left the row last inside the group. A lift may
|
|
9168
|
+
// not cross a DISK boundary (a workspace's folder is a
|
|
9169
|
+
// filesystem fact), so the target level must live in the same
|
|
9170
|
+
// disk scope as the source. The middle band keeps its old
|
|
9171
|
+
// meaning: move the workspace INTO this group.
|
|
9172
|
+
if (zone === null) {
|
|
9173
|
+
if (drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) {
|
|
9174
|
+
event.preventDefault()
|
|
9175
|
+
event.stopPropagation()
|
|
9176
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
9177
|
+
}
|
|
9178
|
+
return
|
|
9179
|
+
}
|
|
9180
|
+
if (zone !== 'into' && (drag.source.levelKey || '') !== parentKeyOf(idPath) && (ownerPrefixOf(idPath) || '') !== (drag.source.disk || '')) { clearOver('folder', idPath); return }
|
|
9181
|
+
}
|
|
8800
9182
|
event.preventDefault()
|
|
8801
9183
|
event.stopPropagation()
|
|
8802
9184
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8803
|
-
|
|
9185
|
+
const reorder = zone !== 'into'
|
|
9186
|
+
const nextOver = reorder
|
|
9187
|
+
? { kind: 'folder', target: idPath, zone }
|
|
9188
|
+
: { kind: 'folder', target: idPath, zone: 'into' }
|
|
9189
|
+
setDrag(current => (current && current.over && current.over.kind === 'folder' && current.over.target === idPath && current.over.zone === nextOver.zone)
|
|
8804
9190
|
? current
|
|
8805
|
-
: (current ? { ...current, over:
|
|
9191
|
+
: (current ? { ...current, over: nextOver } : current))
|
|
8806
9192
|
},
|
|
8807
9193
|
onDrop: (event) => {
|
|
9194
|
+
if (dragMatches('folder')) {
|
|
9195
|
+
event.preventDefault()
|
|
9196
|
+
event.stopPropagation()
|
|
9197
|
+
const zone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath ? drag.over.zone : 'into'
|
|
9198
|
+
if (zone === 'before' || zone === 'after') commitFolderReorder(drag.source, idPath, zone, node)
|
|
9199
|
+
else commitFolderMoveInto(drag.source, idPath)
|
|
9200
|
+
return
|
|
9201
|
+
}
|
|
8808
9202
|
if (!dragMatches('workspace')) return
|
|
8809
9203
|
event.preventDefault()
|
|
8810
9204
|
event.stopPropagation()
|
|
8811
|
-
|
|
9205
|
+
// The live {over} wins; the geometry is the fallback for a drop that
|
|
9206
|
+
// lands before any hover frame recorded a band on this row.
|
|
9207
|
+
const overZone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after') ? drag.over.zone : null
|
|
9208
|
+
const geoZone = rowZone(event) || 'into'
|
|
9209
|
+
const zone = overZone || geoZone
|
|
9210
|
+
if (zone === 'before' || zone === 'after') {
|
|
9211
|
+
// Same level = plain mixed reorder; another level (same disk
|
|
9212
|
+
// scope) = lift the workspace out of its group into that level.
|
|
9213
|
+
if ((drag.source.levelKey || '') === parentKeyOf(idPath)) commitLevelReorder({ kind: 'workspace', source: drag.source }, { kind: 'folder', target: idPath }, zone)
|
|
9214
|
+
else commitWorkspaceLift(drag.source, idPath, zone)
|
|
9215
|
+
} else commitWorkspaceMoveInto(idPath)
|
|
8812
9216
|
},
|
|
8813
9217
|
})
|
|
8814
9218
|
const nextWorkspaceAfter = (folderPath, workspaceId) => {
|
|
@@ -8817,6 +9221,87 @@ window.__ModuleLoader__.load({
|
|
|
8817
9221
|
const index = node.workspaces.findIndex(w => w.workspaceId === workspaceId)
|
|
8818
9222
|
return index === -1 ? undefined : (node.workspaces[index + 1] ? node.workspaces[index + 1].workspaceId : undefined)
|
|
8819
9223
|
}
|
|
9224
|
+
// v0.36.5 MIXED LEVEL REORDER. One persisted list per level decides the
|
|
9225
|
+
// display order of BOTH kinds: a group name token and a "w:<id>"
|
|
9226
|
+
// workspace token. Writes the list only — the registry keeps its own
|
|
9227
|
+
// (workspace-only) order, because "above that group" is a VIEW position
|
|
9228
|
+
// the registry has no vocabulary for: a workspace title still decides
|
|
9229
|
+
// which group it belongs to, the list only decides where the row sits.
|
|
9230
|
+
// v0.36.7: a group block dropped INTO the container it already lives in
|
|
9231
|
+
// (the framed INTO on its DIRECT parent group row, or on the disk-level
|
|
9232
|
+
// row that owns it) lands at that container's TAIL — the same
|
|
9233
|
+
// "放入=排到最后" a workspace row gets when dropped INTO its own group.
|
|
9234
|
+
// No prefix is rewritten (the block already lives under the right path),
|
|
9235
|
+
// only the level order moves.
|
|
9236
|
+
const moveFolderToLevelEnd = (source) => {
|
|
9237
|
+
const containerKey = source.levelKey || ''
|
|
9238
|
+
const container = folderContainerOf(containerKey)
|
|
9239
|
+
if (!container) { console.warn('[bw] folder move refused: target level not found', containerKey); return false }
|
|
9240
|
+
const token = namePathOf(source.idPath).split('/').pop()
|
|
9241
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9242
|
+
if (!tokens.includes(token)) { console.warn('[bw] folder move refused: group not in this level', token); return false }
|
|
9243
|
+
const next = tokens.filter((entry) => entry !== token)
|
|
9244
|
+
next.push(token)
|
|
9245
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, next)
|
|
9246
|
+
return true
|
|
9247
|
+
}
|
|
9248
|
+
const commitFolderMoveToLevelEnd = (source) => {
|
|
9249
|
+
setDrag(null)
|
|
9250
|
+
moveFolderToLevelEnd(source)
|
|
9251
|
+
}
|
|
9252
|
+
const writeLevelOrder = (containerKey, sourceToken, targetToken, zone) => {
|
|
9253
|
+
const container = folderContainerOf(containerKey)
|
|
9254
|
+
if (!container) { console.warn('[bw] level reorder refused: container not found', containerKey); return false }
|
|
9255
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9256
|
+
const at = tokens.indexOf(sourceToken)
|
|
9257
|
+
if (at === -1) { console.warn('[bw] level reorder refused: source not in this level', sourceToken); return false }
|
|
9258
|
+
tokens.splice(at, 1)
|
|
9259
|
+
const targetAt = tokens.indexOf(targetToken)
|
|
9260
|
+
if (targetAt === -1) { console.warn('[bw] level reorder refused: target not in this level', targetToken); return false }
|
|
9261
|
+
tokens.splice(zone === 'after' ? targetAt + 1 : targetAt, 0, sourceToken)
|
|
9262
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens)
|
|
9263
|
+
return true
|
|
9264
|
+
}
|
|
9265
|
+
// v0.36.6 LIFT writes: the source token does NOT exist in the target
|
|
9266
|
+
// level yet (that is what a lift is), so writeLevelOrder's
|
|
9267
|
+
// "source must already be here" precondition does not apply — the token
|
|
9268
|
+
// is spliced straight in beside the target group.
|
|
9269
|
+
const insertLevelToken = (containerKey, sourceToken, targetToken, zone) => {
|
|
9270
|
+
const container = folderContainerOf(containerKey)
|
|
9271
|
+
if (!container) { console.warn('[bw] workspace lift refused: target level not found', containerKey); return false }
|
|
9272
|
+
const tokens = levelSeqOf(container).map(levelTokenOf).filter((token) => token !== sourceToken)
|
|
9273
|
+
const targetAt = tokens.indexOf(targetToken)
|
|
9274
|
+
if (targetAt === -1) { console.warn('[bw] workspace lift refused: target group not in this level', targetToken); return false }
|
|
9275
|
+
tokens.splice(zone === 'after' ? targetAt + 1 : targetAt, 0, sourceToken)
|
|
9276
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens)
|
|
9277
|
+
return true
|
|
9278
|
+
}
|
|
9279
|
+
// The level a lifted workspace LEFT keeps a stale "w:<id>" token
|
|
9280
|
+
// otherwise (buildTree skips it harmlessly, but the list would grow
|
|
9281
|
+
// forever and a later re-group could resurrect the old position).
|
|
9282
|
+
const removeLevelToken = (containerKey, token) => {
|
|
9283
|
+
const container = folderContainerOf(containerKey)
|
|
9284
|
+
if (!container) return
|
|
9285
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9286
|
+
if (!tokens.includes(token)) return
|
|
9287
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens.filter((entry) => entry !== token))
|
|
9288
|
+
}
|
|
9289
|
+
// Shared commit for both mixed directions — a workspace sliding beside a
|
|
9290
|
+
// group row, and a group block sliding beside a workspace row. The
|
|
9291
|
+
// target names the level it belongs to; the hover gate already refused
|
|
9292
|
+
// cross-level targets and this re-checks it as belt and braces.
|
|
9293
|
+
const commitLevelReorder = (sourceRef, targetRef, zone) => {
|
|
9294
|
+
setDrag(null)
|
|
9295
|
+
if (!canDragWorkspace) { console.warn('[bw] level reorder refused: workspace reorder unavailable on this host'); return }
|
|
9296
|
+
const wsSource = sourceRef.kind === 'workspace'
|
|
9297
|
+
const sourceToken = wsSource ? 'w:' + sourceRef.source.workspaceId : namePathOf(sourceRef.source.idPath).split('/').pop()
|
|
9298
|
+
const folderTarget = targetRef.kind === 'folder'
|
|
9299
|
+
const targetToken = folderTarget ? namePathOf(targetRef.target).split('/').pop() : 'w:' + targetRef.workspace.workspaceId
|
|
9300
|
+
const containerKey = folderTarget ? parentKeyOf(targetRef.target) : (targetRef.workspace.levelKey || '')
|
|
9301
|
+
const sourceLevelKey = sourceRef.source.levelKey || ''
|
|
9302
|
+
if (sourceLevelKey !== containerKey) { console.warn('[bw] level reorder refused: cross-level target', sourceLevelKey, containerKey); return }
|
|
9303
|
+
writeLevelOrder(containerKey, sourceToken, targetToken, zone)
|
|
9304
|
+
}
|
|
8820
9305
|
const commitWorkspaceDrop = (targetWorkspace, half) => {
|
|
8821
9306
|
const source = drag.source
|
|
8822
9307
|
setDrag(null)
|
|
@@ -8830,6 +9315,16 @@ window.__ModuleLoader__.load({
|
|
|
8830
9315
|
// cross-level drop only re-groups the title (the registry append
|
|
8831
9316
|
// lands it at the end of ITS disk level).
|
|
8832
9317
|
const sameDisk = source.disk === diskParentMap.get(targetWorkspace.workspaceId)
|
|
9318
|
+
// v0.36.6: a cross-LEVEL drop on a workspace row's half is a PLACEMENT,
|
|
9319
|
+
// not a re-group. The user reported "the insert line shows but the row
|
|
9320
|
+
// lands somewhere else": renaming into the target's group moved the row
|
|
9321
|
+
// into that group's block at whatever position the group already had.
|
|
9322
|
+
// Lifting to the target row's own level and anchoring at that row is
|
|
9323
|
+
// what makes the drop land where the line was drawn.
|
|
9324
|
+
if (workspaceSlash && (targetWorkspace.levelKey || '') !== (source.levelKey || '')) {
|
|
9325
|
+
commitWorkspaceLift(source, targetWorkspace.levelKey || '', half === 'after' ? 'after' : 'before', targetWorkspace.workspaceId)
|
|
9326
|
+
return
|
|
9327
|
+
}
|
|
8833
9328
|
const chain = sameFolder
|
|
8834
9329
|
? Promise.resolve()
|
|
8835
9330
|
: Promise.resolve().then(() => {
|
|
@@ -8843,6 +9338,69 @@ window.__ModuleLoader__.load({
|
|
|
8843
9338
|
: targetWorkspace.workspaceId)
|
|
8844
9339
|
chain
|
|
8845
9340
|
.then(() => (anchor !== undefined ? insertWorkspaceBefore(source.workspaceId, anchor) : insertWorkspaceBefore(source.workspaceId)))
|
|
9341
|
+
// v0.36.5: once a level carries a MIXED list (it interleaves a bare
|
|
9342
|
+
// workspace with its groups), the registry edit above no longer
|
|
9343
|
+
// moves the row — rewrite the list too, keeping every other token.
|
|
9344
|
+
.then(() => {
|
|
9345
|
+
const levelKey = targetWorkspace.levelKey || ''
|
|
9346
|
+
const container = folderContainerOf(levelKey)
|
|
9347
|
+
const tokens = container ? levelSeqOf(container).map(levelTokenOf) : []
|
|
9348
|
+
if (tokens.some((token) => token.startsWith('w:'))) {
|
|
9349
|
+
writeLevelOrder(levelKey, 'w:' + source.workspaceId, 'w:' + targetWorkspace.workspaceId, half === 'after' ? 'after' : 'before')
|
|
9350
|
+
}
|
|
9351
|
+
})
|
|
9352
|
+
.catch(fail)
|
|
9353
|
+
}
|
|
9354
|
+
// v0.36.6 LIFT. Dropping a bare workspace row on a group's EDGE band
|
|
9355
|
+
// from a DIFFERENT level means: take the row out of its group, give it
|
|
9356
|
+
// the target level's name prefix and place it right beside that group.
|
|
9357
|
+
// The user's report was "I can never insert it as the first row" — the
|
|
9358
|
+
// gesture existed visually but every path landed it inside the group
|
|
9359
|
+
// (move-into) at the tail. The lift is a rename + a level token, never a
|
|
9360
|
+
// disk move, so the caller has already checked the disk scope.
|
|
9361
|
+
const commitWorkspaceLift = (source, targetIdPath, zone, anchorWorkspaceId) => {
|
|
9362
|
+
setDrag(null)
|
|
9363
|
+
if (!canDragWorkspace) { console.warn('[bw] workspace lift refused: workspace reorder unavailable on this host'); return }
|
|
9364
|
+
const targetParentKey = parentKeyOf(targetIdPath)
|
|
9365
|
+
if (!folderContainerOf(targetParentKey)) { console.warn('[bw] workspace lift refused: target level not found', targetParentKey); return }
|
|
9366
|
+
const cutAt = targetParentKey.lastIndexOf('//')
|
|
9367
|
+
const levelNamePath = cutAt === -1 ? targetParentKey : targetParentKey.slice(cutAt + 2)
|
|
9368
|
+
const sourceKey = source.levelKey || ''
|
|
9369
|
+
const targetNamePath = namePathOf(targetIdPath)
|
|
9370
|
+
const targetNode = findTreeNode(rawTree, targetNamePath)
|
|
9371
|
+
const targetMembers = targetNode ? collectMemberIds(targetNode) : []
|
|
9372
|
+
const registryIds = (items || []).map((w) => w.workspaceId)
|
|
9373
|
+
// after = insert before whatever follows the target block in the
|
|
9374
|
+
// registry (undefined = append). before = before the block's head.
|
|
9375
|
+
// A row-anchored lift (dropped on a workspace row's half) anchors at
|
|
9376
|
+
// that row itself, not at the head/tail of its whole group block.
|
|
9377
|
+
let anchor
|
|
9378
|
+
if (anchorWorkspaceId) {
|
|
9379
|
+
if (zone === 'before') {
|
|
9380
|
+
anchor = anchorWorkspaceId
|
|
9381
|
+
} else {
|
|
9382
|
+
const rowAt = registryIds.indexOf(anchorWorkspaceId)
|
|
9383
|
+
anchor = rowAt === -1 ? undefined : registryIds[rowAt + 1]
|
|
9384
|
+
}
|
|
9385
|
+
} else if (zone === 'before') {
|
|
9386
|
+
anchor = targetMembers[0]
|
|
9387
|
+
} else if (targetMembers.length) {
|
|
9388
|
+
const tailAt = registryIds.indexOf(targetMembers[targetMembers.length - 1])
|
|
9389
|
+
anchor = tailAt === -1 ? undefined : registryIds[tailAt + 1]
|
|
9390
|
+
}
|
|
9391
|
+
const newTitle = levelNamePath !== '' ? levelNamePath + '/' + source.leaf : source.leaf
|
|
9392
|
+
const sameNameLevel = (source.folderPath || '') === levelNamePath
|
|
9393
|
+
Promise.resolve()
|
|
9394
|
+
.then(() => (sameNameLevel ? Promise.resolve() : renameWorkspace(source.workspaceId, newTitle)))
|
|
9395
|
+
.then(() => insertWorkspaceBefore(source.workspaceId, anchor))
|
|
9396
|
+
.then(() => {
|
|
9397
|
+
// The level token is always a GROUP token: a workspace row dropped on
|
|
9398
|
+
// from another level is painted beside the group that row lives in,
|
|
9399
|
+
// so the token to place it next to is that group's name in the
|
|
9400
|
+
// parent level's list (the row's own "w:" token lives one level in).
|
|
9401
|
+
insertLevelToken(targetParentKey, 'w:' + source.workspaceId, targetNamePath.split('/').pop(), zone)
|
|
9402
|
+
if (sourceKey !== targetParentKey) removeLevelToken(sourceKey, 'w:' + source.workspaceId)
|
|
9403
|
+
})
|
|
8846
9404
|
.catch(fail)
|
|
8847
9405
|
}
|
|
8848
9406
|
const commitWorkspaceMoveInto = (folderIdPath) => {
|
|
@@ -8859,6 +9417,102 @@ window.__ModuleLoader__.load({
|
|
|
8859
9417
|
.then(() => insertWorkspaceBefore(source.workspaceId))
|
|
8860
9418
|
.catch(fail)
|
|
8861
9419
|
}
|
|
9420
|
+
// Group drag (v0.36.0): moving a folder rewrites EVERY member's title
|
|
9421
|
+
// under the target prefix — the same batch rename the rename-group
|
|
9422
|
+
// dialog (submitFolderRename) performs. Titles are global name paths,
|
|
9423
|
+
// so the member sweep is prefix-based over all items (same known
|
|
9424
|
+
// limitation as that dialog: same-named groups on different disk
|
|
9425
|
+
// levels move together). Self / descendant targets are already
|
|
9426
|
+
// refused at hover time; re-checked here as belt and braces.
|
|
9427
|
+
const commitFolderMoveInto = (source, targetIdPath) => {
|
|
9428
|
+
setDrag(null)
|
|
9429
|
+
const targetPath = namePathOf(targetIdPath)
|
|
9430
|
+
// v0.36.7: the DIRECT parent is the container the group already lives
|
|
9431
|
+
// in. The hover already showed the framed INTO there, so "放进那个
|
|
9432
|
+
// 组" means "move to the end of that group" — no rename involved.
|
|
9433
|
+
// Checked BEFORE the ancestor refusal below.
|
|
9434
|
+
const cutAt = source.path.lastIndexOf('/')
|
|
9435
|
+
const directParent = cutAt === -1 ? '' : source.path.slice(0, cutAt)
|
|
9436
|
+
if (targetPath === directParent) { moveFolderToLevelEnd(source); return }
|
|
9437
|
+
// An ANCESTOR target means the group already lives there: a prefix
|
|
9438
|
+
// rewrite would duplicate a segment (工作/A → 工作/工作/A). Refuse.
|
|
9439
|
+
if (source.path === targetPath || targetPath.startsWith(source.path + '/') || (targetPath !== '' && source.path.startsWith(targetPath + '/'))) { console.warn('[bw] folder move refused: target is self, descendant or ancestor of', source.path); return }
|
|
9440
|
+
const prefix = source.path + '/'
|
|
9441
|
+
const affected = (items || []).filter((w) => String(w.title || '').startsWith(prefix))
|
|
9442
|
+
if (!affected.length) { console.warn('[bw] folder move refused: no members under', source.path); return }
|
|
9443
|
+
Promise.resolve()
|
|
9444
|
+
.then(async () => {
|
|
9445
|
+
for (const w of affected) {
|
|
9446
|
+
await renameWorkspace(w.workspaceId, targetPath + String(w.title).slice(source.path.length))
|
|
9447
|
+
}
|
|
9448
|
+
})
|
|
9449
|
+
// (v0.36.4) NO automatic expand after a move-into. v0.36.3 opened
|
|
9450
|
+
// the target group here so the move would be visible, but on the
|
|
9451
|
+
// real tree a drop in the middle band is exactly the "变下级"
|
|
9452
|
+
// gesture, and auto-opening made that read as "又被撑开了".
|
|
9453
|
+
// The row the user just dropped into stays as it was; the moved
|
|
9454
|
+
// members appear under it once the user opens that group.
|
|
9455
|
+
.catch(fail)
|
|
9456
|
+
}
|
|
9457
|
+
// Group REORDER (v0.36.0): a name group's position IS the registry
|
|
9458
|
+
// order of its members, so reordering = sliding the whole member
|
|
9459
|
+
// block (prefixes untouched) to just before / after the target
|
|
9460
|
+
// group's block. Anchored like commitWorkspaceDrop: before = the
|
|
9461
|
+
// target's first member, after = whatever follows the target's last
|
|
9462
|
+
// member (undefined = block-append at the level tail). Hover only
|
|
9463
|
+
// arms this between same-level groups (identical owner prefix);
|
|
9464
|
+
// re-checked here as belt and braces.
|
|
9465
|
+
const commitFolderReorder = (source, targetIdPath, zone, targetNode) => {
|
|
9466
|
+
setDrag(null)
|
|
9467
|
+
if (!canDragWorkspace) { console.warn('[bw] folder reorder refused: workspace reorder unavailable on this host'); return }
|
|
9468
|
+
if (ownerPrefixOf(source.idPath) !== ownerPrefixOf(targetIdPath)) { console.warn('[bw] folder reorder refused: cross-level target'); return }
|
|
9469
|
+
// v0.36.4: SIBLINGS ONLY. A group's rendered position is decided by
|
|
9470
|
+
// its parent's order list, so a target that hangs off a different
|
|
9471
|
+
// parent cannot place it at all.
|
|
9472
|
+
if (parentKeyOf(source.idPath) !== parentKeyOf(targetIdPath)) { console.warn('[bw] folder reorder refused: cross-parent target'); return }
|
|
9473
|
+
const sourceIds = source.memberIds || []
|
|
9474
|
+
if (!sourceIds.length) { console.warn('[bw] folder reorder refused: empty source block'); return }
|
|
9475
|
+
// Whole-block anchors: the target's members include its nested
|
|
9476
|
+
// sub-groups (a plain .workspaces read misses container groups and
|
|
9477
|
+
// silently returned on them — the "drop did nothing" report).
|
|
9478
|
+
// v0.36.3 drag-out: the source block IS part of the target's block
|
|
9479
|
+
// (dropping a nested group onto its parent), so the source subtree
|
|
9480
|
+
// is filtered out before anchoring — otherwise the anchor lands
|
|
9481
|
+
// inside the moving subtree and the drop no-ops or oscillates.
|
|
9482
|
+
const sourceSet = new Set(sourceIds)
|
|
9483
|
+
const targetMembers = collectMemberIds(targetNode).filter((id) => !sourceSet.has(id))
|
|
9484
|
+
if (!targetMembers.length) { console.warn('[bw] folder reorder refused: target has no members to anchor against'); return }
|
|
9485
|
+
// before = the surviving block's first member; after = its last
|
|
9486
|
+
// member (inserting before it = right after the block). Anchoring
|
|
9487
|
+
// on the block tail itself replaces nextWorkspaceAfter, whose
|
|
9488
|
+
// node.workspaces lookup missed nested tails (findIndex -1 →
|
|
9489
|
+
// silent append).
|
|
9490
|
+
const anchor = zone === 'after'
|
|
9491
|
+
? targetMembers[targetMembers.length - 1]
|
|
9492
|
+
: targetMembers[0]
|
|
9493
|
+
// v0.36.4: THE DISPLAYED ORDER IS PERSISTED HERE. Group order used to
|
|
9494
|
+
// follow buildTree's name sort, so the registry edit below changed
|
|
9495
|
+
// nothing the user could see ("拖了没反应"). folderOrder[parentKey]
|
|
9496
|
+
// is the sibling name list buildTree sorts by; write it first so the
|
|
9497
|
+
// drop is visible immediately, then sync the registry for the
|
|
9498
|
+
// workspace order other surfaces read.
|
|
9499
|
+
const orderParent = parentKeyOf(source.idPath)
|
|
9500
|
+
const sourceName = namePathOf(source.idPath).split('/').pop()
|
|
9501
|
+
const targetName = namePathOf(targetIdPath).split('/').pop()
|
|
9502
|
+
// v0.36.5: write the MIXED token list (group names + w:<id> tokens),
|
|
9503
|
+
// not just the folder names — a level that already interleaves a bare
|
|
9504
|
+
// workspace with its groups must keep those workspace positions.
|
|
9505
|
+
writeLevelOrder(orderParent, sourceName, targetName, zone)
|
|
9506
|
+
Promise.resolve()
|
|
9507
|
+
.then(async () => {
|
|
9508
|
+
if (anchor === undefined) {
|
|
9509
|
+
for (const id of sourceIds) await insertWorkspaceBefore(id)
|
|
9510
|
+
} else {
|
|
9511
|
+
for (const id of sourceIds) await insertWorkspaceBefore(id, anchor)
|
|
9512
|
+
}
|
|
9513
|
+
})
|
|
9514
|
+
.catch(fail)
|
|
9515
|
+
}
|
|
8862
9516
|
|
|
8863
9517
|
/* ---------------------- session drag & drop --------------------- */
|
|
8864
9518
|
|
|
@@ -8986,6 +9640,10 @@ window.__ModuleLoader__.load({
|
|
|
8986
9640
|
.catch(fail)
|
|
8987
9641
|
}
|
|
8988
9642
|
const wsRootDropActive = (workspaceId) => dragMatches('session') && drag.over !== null && drag.over.kind === 'ws-root' && drag.over.target === workspaceId
|
|
9643
|
+
// v0.36.7: a group block hovering the disk-level row that OWNS it lights
|
|
9644
|
+
// the SAME framed INTO a workspace row gets on a group row — the visual
|
|
9645
|
+
// is one language: edge bands reorder, the middle band "puts inside".
|
|
9646
|
+
const wsGroupIntoActive = (workspaceId) => dragMatches('folder') && drag.over !== null && drag.over.kind === 'ws-level' && drag.over.target === workspaceId
|
|
8989
9647
|
const commitSessionMoveInto = (workspaceId, groupPath) => {
|
|
8990
9648
|
const source = drag.source
|
|
8991
9649
|
setDrag(null)
|
|
@@ -9245,16 +9903,14 @@ window.__ModuleLoader__.load({
|
|
|
9245
9903
|
// SEE the new session appear, even if the row was collapsed.
|
|
9246
9904
|
onStart: () => { actions.setSessionsExpanded(workspace.workspaceId, true); startSession(workspace.workspaceId) },
|
|
9247
9905
|
onContextMenu: (e) => openCtx('workspace', workspace, e),
|
|
9248
|
-
//
|
|
9249
|
-
//
|
|
9250
|
-
//
|
|
9251
|
-
//
|
|
9252
|
-
onMenu: (e) => openCtx('workspace', workspace, e),
|
|
9906
|
+
// The in-row ellipsis is retired; the ctx surface stays the
|
|
9907
|
+
// right-click path. While the menu is open the row still pins
|
|
9908
|
+
// (hover fill + visible actions), matching the official menuOpen
|
|
9909
|
+
// state.
|
|
9253
9910
|
menuOpen: !!(ctx && ctx.kind === 'workspace' && ctx.payload && ctx.payload.workspaceId === workspace.workspaceId),
|
|
9254
|
-
menuAria: officialText('actions.workspace.aria', { name: workspace.title || workspace.leaf }) || undefined,
|
|
9255
9911
|
hoverBody: workspaceHoverBody,
|
|
9256
9912
|
dropHalf: wsDropHalf(workspace.workspaceId),
|
|
9257
|
-
dropInto: wsRootDropActive(workspace.workspaceId),
|
|
9913
|
+
dropInto: wsRootDropActive(workspace.workspaceId) || wsGroupIntoActive(workspace.workspaceId),
|
|
9258
9914
|
dragEvents: workspaceDragEvents(workspace),
|
|
9259
9915
|
custStyle: rowStyleOf('workspace:' + workspace.workspaceId),
|
|
9260
9916
|
iconMode: (styleEntry('workspace:' + workspace.workspaceId) || {}).icon || 'solid',
|
|
@@ -9298,8 +9954,11 @@ window.__ModuleLoader__.load({
|
|
|
9298
9954
|
: undefined
|
|
9299
9955
|
const renderLevelRows = (level, depth) => {
|
|
9300
9956
|
const rows = []
|
|
9301
|
-
for (const
|
|
9302
|
-
|
|
9957
|
+
for (const item of levelSeqOf(level)) {
|
|
9958
|
+
rows.push(...(item.kind === 'ws'
|
|
9959
|
+
? renderWorkspaceEntry({ workspace: item.entry }, depth, wsPulseOf(item.entry))
|
|
9960
|
+
: renderPlainFolder(item.node, depth)))
|
|
9961
|
+
}
|
|
9303
9962
|
return rows
|
|
9304
9963
|
}
|
|
9305
9964
|
|
|
@@ -9320,7 +9979,8 @@ window.__ModuleLoader__.load({
|
|
|
9320
9979
|
onToggle: () => { if (!searching) actions.setExpanded(idPath, !expanded) },
|
|
9321
9980
|
onContextMenu: (e) => openCtx('folder', { path: node.path, name: node.name, idPath, scope: node.scope, empty: countWorkspaces(node) === 0 }, e),
|
|
9322
9981
|
dropInto: wsDropInto(idPath),
|
|
9323
|
-
|
|
9982
|
+
dropSlot: wsFolderSlotOf(idPath),
|
|
9983
|
+
dragEvents: folderDropEvents(idPath, node.path, node),
|
|
9324
9984
|
custStyle: rowStyleOf('folder:' + idPath),
|
|
9325
9985
|
iconMode: (styleEntry('folder:' + idPath) || {}).icon || 'solid',
|
|
9326
9986
|
pulse: expanded ? null : folderPulseOf(node),
|
|
@@ -9328,8 +9988,11 @@ window.__ModuleLoader__.load({
|
|
|
9328
9988
|
t,
|
|
9329
9989
|
})]
|
|
9330
9990
|
if (expanded) {
|
|
9331
|
-
for (const
|
|
9332
|
-
|
|
9991
|
+
for (const item of levelSeqOf(node)) {
|
|
9992
|
+
rows.push(...(item.kind === 'ws'
|
|
9993
|
+
? renderWorkspaceEntry({ workspace: item.entry }, depth + 1, wsPulseOf(item.entry))
|
|
9994
|
+
: renderPlainFolder(item.node, depth + 1)))
|
|
9995
|
+
}
|
|
9333
9996
|
}
|
|
9334
9997
|
return rows
|
|
9335
9998
|
}
|
|
@@ -9384,8 +10047,11 @@ window.__ModuleLoader__.load({
|
|
|
9384
10047
|
}
|
|
9385
10048
|
bodyRows.push(...renderUngroupedBlock())
|
|
9386
10049
|
} else {
|
|
9387
|
-
for (const
|
|
9388
|
-
|
|
10050
|
+
for (const item of levelSeqOf(tree)) {
|
|
10051
|
+
bodyRows.push(...(item.kind === 'ws'
|
|
10052
|
+
? renderWorkspaceEntry({ workspace: item.entry }, 0, wsPulseOf(item.entry))
|
|
10053
|
+
: renderPlainFolder(item.node, 0)))
|
|
10054
|
+
}
|
|
9389
10055
|
bodyRows.push(...renderUngroupedBlock())
|
|
9390
10056
|
}
|
|
9391
10057
|
const isEmpty = bodyRows.length === 0
|
|
@@ -9564,8 +10230,8 @@ window.__ModuleLoader__.load({
|
|
|
9564
10230
|
]
|
|
9565
10231
|
if (ctx.kind === 'session') {
|
|
9566
10232
|
// The shared builder owns the session list (see
|
|
9567
|
-
// sessionMenuEntriesFor): the
|
|
9568
|
-
//
|
|
10233
|
+
// sessionMenuEntriesFor): the ctx menu and any future surface share
|
|
10234
|
+
// it, so the entries cannot drift.
|
|
9569
10235
|
return sessionMenuEntriesFor(ctx.payload || {})
|
|
9570
10236
|
}
|
|
9571
10237
|
return [
|
|
@@ -10258,33 +10924,54 @@ window.__ModuleLoader__.load({
|
|
|
10258
10924
|
if (!result || !result.ok) throw new Error(result && result.error ? result.error.message : 'session search failed')
|
|
10259
10925
|
return result.value
|
|
10260
10926
|
}
|
|
10261
|
-
//
|
|
10262
|
-
//
|
|
10263
|
-
//
|
|
10264
|
-
//
|
|
10265
|
-
// navigation service: ISessions no longer carries open() at all
|
|
10266
|
-
// (navigation belongs to the view owner, replaceMain).
|
|
10927
|
+
// Rename performance (issue #14): sessions.using retains the session and
|
|
10928
|
+
// awaits reference.ready FIRST, so renaming a closed 5MB row stalled ~6s
|
|
10929
|
+
// loading its whole history just to send a 20ms rename RPC (the rename
|
|
10930
|
+
// itself is independent of the transcript). Tiered, cheapest first:
|
|
10267
10931
|
//
|
|
10268
|
-
//
|
|
10269
|
-
//
|
|
10270
|
-
//
|
|
10271
|
-
//
|
|
10272
|
-
//
|
|
10932
|
+
// 1. Open session → borrow binding directly. On 0.1.6+ hosts binding(id)
|
|
10933
|
+
// only sees retained scopes (service.ts:539 `scopes.get(id)?.binding`,
|
|
10934
|
+
// undefined when closed), so "a binding comes back" IS the open test;
|
|
10935
|
+
// on 0.1.5-rc.x hosts every row is visible, identical to the old
|
|
10936
|
+
// fallback. Same SessionBinding object the using path borrows, so
|
|
10937
|
+
// title projection / list refresh behave exactly the same.
|
|
10938
|
+
// 2. Closed session → sessions.manager.rename(id, title): "Rename a
|
|
10939
|
+
// Session ... without opening its history" (manager.ts:578) — a bare
|
|
10940
|
+
// remote.session.rename plus the title projection. manager is a
|
|
10941
|
+
// TS-private field of ClientSessions (service.ts:245), reachable at
|
|
10942
|
+
// runtime; feature-probed so hosts without it skip the tier.
|
|
10943
|
+
// 3. Fallback → the 0.1.6-alpha.2 retention contract: sessions.using
|
|
10944
|
+
// with an explicit source (slow — loads history — but the only legal
|
|
10945
|
+
// path on hosts predating manager.rename). 0.1.5-rc.x hosts ship
|
|
10946
|
+
// ISessions.binding only; tier 1 already served them. Then fail loud.
|
|
10947
|
+
// Fork and open go through the uiWorkspace navigation service: ISessions
|
|
10948
|
+
// no longer carries open() at all (navigation belongs to the view owner,
|
|
10949
|
+
// replaceMain).
|
|
10273
10950
|
const renameSession = async (sessionId, title) => {
|
|
10951
|
+
const fail = (result) => new Error(result && result.error ? result.error.message : 'session rename failed')
|
|
10952
|
+
const binding = typeof sessions.binding === 'function' ? sessions.binding(sessionId) : undefined
|
|
10953
|
+
const face = binding ? binding.session : undefined
|
|
10954
|
+
if (face && typeof face.rename === 'function') {
|
|
10955
|
+
const result = await face.rename(title)
|
|
10956
|
+
if (!result || !result.ok) throw fail(result)
|
|
10957
|
+
return
|
|
10958
|
+
}
|
|
10959
|
+
const manager = sessions.manager
|
|
10960
|
+
if (manager && typeof manager.rename === 'function') {
|
|
10961
|
+
const result = await manager.rename(sessionId, title)
|
|
10962
|
+
if (!result || !result.ok) throw fail(result)
|
|
10963
|
+
return
|
|
10964
|
+
}
|
|
10274
10965
|
if (typeof sessions.using === 'function') {
|
|
10275
10966
|
const result = await sessions.using(
|
|
10276
10967
|
sessionId,
|
|
10277
10968
|
{ source: 'workspaceOperation' },
|
|
10278
10969
|
(reference) => reference.binding.session.rename(title),
|
|
10279
10970
|
)
|
|
10280
|
-
if (!result || !result.ok) throw
|
|
10971
|
+
if (!result || !result.ok) throw fail(result)
|
|
10281
10972
|
return
|
|
10282
10973
|
}
|
|
10283
|
-
|
|
10284
|
-
const face = binding ? binding.session : undefined
|
|
10285
|
-
if (!face || typeof face.rename !== 'function') throw new Error('session rename is unavailable on this host')
|
|
10286
|
-
const result = await face.rename(title)
|
|
10287
|
-
if (!result || !result.ok) throw new Error(result && result.error ? result.error.message : 'session rename failed')
|
|
10974
|
+
throw new Error('session rename is unavailable on this host')
|
|
10288
10975
|
}
|
|
10289
10976
|
const forkSession = (sessionId) => {
|
|
10290
10977
|
uiWorkspace.forkSession(sessionId)
|