dsh-manage-session 0.4.8 → 0.5.3

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.en.md +4 -0
  3. package/README.md +4 -0
  4. package/cordis.patch.yml +6 -1
  5. package/dsh.plugin.json +22 -22
  6. package/lib/client.js +3848 -202
  7. package/lib/index.d.ts +759 -29
  8. package/lib/index.js +2604 -93
  9. package/locale/de.json +6 -0
  10. package/locale/en.json +6 -0
  11. package/locale/es.json +6 -0
  12. package/locale/fr.json +6 -0
  13. package/locale/it.json +6 -0
  14. package/locale/ja.json +6 -0
  15. package/locale/ko.json +6 -0
  16. package/locale/ru.json +6 -0
  17. package/locale/zh.json +6 -0
  18. package/package.json +43 -32
  19. package/src/client/bridge-scope.ts +213 -0
  20. package/src/client/index.ts +154 -26
  21. package/src/client/locales.ts +857 -223
  22. package/src/client/panel.tsx +139 -118
  23. package/src/client/search/card.tsx +563 -0
  24. package/src/client/search/host-api.ts +208 -0
  25. package/src/client/search/locales.ts +780 -0
  26. package/src/client/search/manage.tsx +400 -0
  27. package/src/client/search/panel.tsx +815 -0
  28. package/src/client/search/platform.ts +136 -0
  29. package/src/config.ts +54 -5
  30. package/src/host/history/archive-source.ts +48 -0
  31. package/src/host/history/archive.ts +56 -1
  32. package/src/host/history/favorites.ts +71 -0
  33. package/src/host/index/archive-source.ts +90 -0
  34. package/src/host/index/engine.ts +571 -0
  35. package/src/host/index/extract.ts +272 -0
  36. package/src/host/index/peers.ts +62 -0
  37. package/src/host/index/rebuild.ts +368 -0
  38. package/src/host/index/schema.ts +136 -0
  39. package/src/host/index/snapshot.ts +135 -0
  40. package/src/host/index/sync.ts +286 -0
  41. package/src/host/registry/archive-registry.ts +317 -0
  42. package/src/host/settings-bridge.ts +237 -0
  43. package/src/index.ts +1421 -532
package/lib/client.js CHANGED
@@ -11,7 +11,8 @@ window.__ModuleLoader__.load({
11
11
  const DEFAULT_CONFIG = {
12
12
  enabled: true,
13
13
  historyFiles: true,
14
- healthCheck: true
14
+ healthCheck: true,
15
+ search: true
15
16
  };
16
17
  /** host 半身注册的设置命名空间(与 src/index.ts 保持一致)。 */
17
18
  const STEWARD_SETTINGS_NAMESPACE = "session-steward";
@@ -20,7 +21,7 @@ window.__ModuleLoader__.load({
20
21
  //#endregion
21
22
  //#region src/client/locales.ts
22
23
  /** 简体中文(内置回退字典)。 */
23
- const zh = {
24
+ const zh$1 = {
24
25
  "card.title": "会话管家",
25
26
  "card.desc": "历史文件 · 健康检查",
26
27
  "card.enabled": "启用会话管家",
@@ -35,6 +36,7 @@ window.__ModuleLoader__.load({
35
36
  "panel.title": "会话管家",
36
37
  "panel.tab.history": "养老院",
37
38
  "panel.tab.health": "体检",
39
+ "panel.tab.manage": "管理",
38
40
  "panel.close": "关闭",
39
41
  "panel.untitled": "(无标题)",
40
42
  "history.loading": "正在读取归档列表…",
@@ -106,99 +108,708 @@ window.__ModuleLoader__.load({
106
108
  "health.ago.hours": "{n} 小时前",
107
109
  "health.ago.days": "{n} 天前"
108
110
  };
109
- /** English dictionary (same keys). */
110
- const en = {
111
- "card.title": "Session Steward",
112
- "card.desc": "History files · Health check",
113
- "card.enabled": "Enable Session Steward",
114
- "card.enabled.desc": "When off, no API is registered and the sidebar entry disappears.",
115
- "card.history": "Session history files",
116
- "card.history.desc": "Archive browsing and cleanup (Retirement Home). When off, history routes and the tab are gone.",
117
- "card.health": "Health check",
118
- "card.health.desc": "Checkup → Prescription → Discharge. When off, health routes and the tab are gone.",
119
- "card.readonly": "This session cannot edit settings (read-only mount).",
120
- "card.unavailable": "Settings service unavailable; configuration cannot be read.",
121
- "family.title": "Screwdriver plugin settings",
122
- "panel.title": "Session Steward",
123
- "panel.tab.history": "Retirement Home",
124
- "panel.tab.health": "Checkup",
125
- "panel.close": "Close",
126
- "panel.untitled": "(untitled)",
127
- "history.loading": "Loading archived sessions…",
128
- "history.empty": "The archive set is empty.",
129
- "history.edit": "Edit",
130
- "history.done": "Done",
131
- "history.selectAll": "Select all",
132
- "history.unselectAll": "Clear selection",
133
- "history.unarchive": "Unarchive state ({n})",
134
- "history.unarchiving": "Unarchiving…",
135
- "history.purge": "Purge archive files ({n}) · free {size}",
136
- "history.purging": "Purging…",
137
- "history.editingHint": "Edit mode: tick sessions, then pick one — \"Unarchive state\" only removes them from the official archive array (reversible; they return to the sidebar after a restart); \"Purge archive files\" really deletes the transcript and projection cache from disk (irreversible).",
138
- "history.restartHint": "Removed {removed} id(s) from the archive array ({remaining} left). **Restart DSH now**: until the host exits, any archive or workspace change rewrites the whole in-memory snapshot back to the file and undoes this.",
139
- "history.pendingRestart": "{n} archived id(s) are still live in host memory: the list follows the storage file (they are already gone), but their sessions stay hidden from the sidebar until DSH restarts.",
140
- "history.count": "{n} total · {size} on disk",
141
- "history.size": "transcript {t} · cache {c}",
142
- "history.sizeUnknown": "no on-disk entity",
143
- "history.source.registry": "Source: host archive registry",
144
- "history.source.storage-file": "Source: storage file",
145
- "history.source.none": "The archive set is unreadable from both sources",
146
- "history.confirmUnarchive": "Unarchive these {n} session(s)?\n\n{summary}\n\nOnly the archive array is edited (auto backup); nothing on disk is touched. After a DSH restart these sessions return to their workspaces.",
147
- "history.confirmPurge": "Purge the on-disk files of these {n} archived session(s)?\n\n{summary}\n\nAbout {size} will be freed.\nThis deletes: the transcript directory (including older-format copies) and the per-session projection cache, and removes them from the archive array and the workspace membership tables.\n\n**This cannot be undone — the sessions will be unrecoverable.**",
148
- "history.purgeResult": "Purged {purged} archived session(s), freed {size}. Restart DSH to make them disappear completely.",
149
- "history.purgePartial": "Purged {purged}, freed {size}; {failed} failed (see details below).",
150
- "history.purgeFailures": "Failure details",
151
- "health.scan": "Run checkup",
152
- "health.scanning": "Running…",
153
- "health.progress": "{done}/{total} · {sec}s elapsed",
154
- "health.empty": "No problem session found.",
155
- "health.clean": "All checks passed.",
156
- "health.level.ok": "ok",
157
- "health.level.warn": "warn",
158
- "health.level.skipped": "Not checked",
159
- "health.level.fail": "fail",
160
- "health.priority.high": "Priority",
161
- "health.gate.generation": "Generation artifact",
162
- "health.gate.log-integrity": "Log integrity",
163
- "health.gate.projection-cache": "Projection cache",
164
- "health.gate.lossless-json": "Lossless JSON",
165
- "health.gate.cold-read": "Continuability",
166
- "health.gate.source-kind": "Plugin signature",
167
- "health.attribution": "Owner",
168
- "health.field": "Field",
169
- "health.unknownOwner": "owner unknown",
170
- "health.phase.checkup": "1) Checkup",
171
- "health.phase.prescribe": "2) Prescription",
172
- "health.phase.discharge": "3) Discharge",
173
- "health.repair": "Apply reversible repair",
174
- "health.repairing": "Repairing…",
175
- "health.sourceMigrate": "Migrate legacy signatures",
176
- "health.sourceMigrating": "Migrating…",
177
- "health.sourceMigrated": "Migrated {n} legacy signature rows (backup: {backup})",
178
- "health.sourceMigrateNone": "No legacy signature rows to migrate",
179
- "health.sourceMigrateFail": "Migration failed: {error}",
180
- "health.before": "Before",
181
- "health.after": "After",
182
- "health.quarantined": "Projection cache quarantined (refolds after a DSH restart)",
183
- "health.commands": "Commands",
184
- "health.copy": "Copy",
185
- "health.copied": "Copied",
186
- "health.detail": "Details",
187
- "health.back": "Back",
188
- "health.cache.hint": "Cached result · {ago}",
189
- "health.cache.refresh": "Refresh",
190
- "health.cache.corpusChanged": "Corpus changed ({was} → {now}); refresh recommended",
191
- "health.ago.justNow": "just now",
192
- "health.ago.minutes": "{n} min ago",
193
- "health.ago.hours": "{n} h ago",
194
- "health.ago.days": "{n} d ago"
111
+ /**
112
+ * Todas las locales / 全部语言,按 locale 服务消费的形态组合。
113
+ * 单次注册即可激活全部 9 种语言(zh/en 为宿主内置,其余走同一 map 重载)。
114
+ */
115
+ const dictionaries$1 = {
116
+ zh: zh$1,
117
+ en: {
118
+ "card.title": "Session Steward",
119
+ "card.desc": "History files · Health check",
120
+ "card.enabled": "Enable Session Steward",
121
+ "card.enabled.desc": "When off, no API is registered and the sidebar entry disappears.",
122
+ "card.history": "Session history files",
123
+ "card.history.desc": "Archive browsing and cleanup (Retirement Home). When off, history routes and the tab are gone.",
124
+ "card.health": "Health check",
125
+ "card.health.desc": "Checkup → Prescription → Discharge. When off, health routes and the tab are gone.",
126
+ "card.readonly": "This session cannot edit settings (read-only mount).",
127
+ "card.unavailable": "Settings service unavailable; configuration cannot be read.",
128
+ "family.title": "Screwdriver plugin settings",
129
+ "panel.title": "Session Steward",
130
+ "panel.tab.history": "Retirement Home",
131
+ "panel.tab.health": "Checkup",
132
+ "panel.tab.manage": "Manage",
133
+ "panel.close": "Close",
134
+ "panel.untitled": "(untitled)",
135
+ "history.loading": "Loading archived sessions…",
136
+ "history.empty": "The archive set is empty.",
137
+ "history.edit": "Edit",
138
+ "history.done": "Done",
139
+ "history.selectAll": "Select all",
140
+ "history.unselectAll": "Clear selection",
141
+ "history.unarchive": "Unarchive state ({n})",
142
+ "history.unarchiving": "Unarchiving…",
143
+ "history.purge": "Purge archive files ({n}) · free {size}",
144
+ "history.purging": "Purging…",
145
+ "history.editingHint": "Edit mode: tick sessions, then pick one — \"Unarchive state\" only removes them from the official archive array (reversible; they return to the sidebar after a restart); \"Purge archive files\" really deletes the transcript and projection cache from disk (irreversible).",
146
+ "history.restartHint": "Removed {removed} id(s) from the archive array ({remaining} left). **Restart DSH now**: until the host exits, any archive or workspace change rewrites the whole in-memory snapshot back to the file and undoes this.",
147
+ "history.pendingRestart": "{n} archived id(s) are still live in host memory: the list follows the storage file (they are already gone), but their sessions stay hidden from the sidebar until DSH restarts.",
148
+ "history.count": "{n} total · {size} on disk",
149
+ "history.size": "transcript {t} · cache {c}",
150
+ "history.sizeUnknown": "no on-disk entity",
151
+ "history.source.registry": "Source: host archive registry",
152
+ "history.source.storage-file": "Source: storage file",
153
+ "history.source.none": "The archive set is unreadable from both sources",
154
+ "history.confirmUnarchive": "Unarchive these {n} session(s)?\n\n{summary}\n\nOnly the archive array is edited (auto backup); nothing on disk is touched. After a DSH restart these sessions return to their workspaces.",
155
+ "history.confirmPurge": "Purge the on-disk files of these {n} archived session(s)?\n\n{summary}\n\nAbout {size} will be freed.\nThis deletes: the transcript directory (including older-format copies) and the per-session projection cache, and removes them from the archive array and the workspace membership tables.\n\n**This cannot be undone — the sessions will be unrecoverable.**",
156
+ "history.purgeResult": "Purged {purged} archived session(s), freed {size}. Restart DSH to make them disappear completely.",
157
+ "history.purgePartial": "Purged {purged}, freed {size}; {failed} failed (see details below).",
158
+ "history.purgeFailures": "Failure details",
159
+ "health.scan": "Run checkup",
160
+ "health.scanning": "Running…",
161
+ "health.progress": "{done}/{total} · {sec}s elapsed",
162
+ "health.empty": "No problem session found.",
163
+ "health.clean": "All checks passed.",
164
+ "health.level.ok": "ok",
165
+ "health.level.warn": "warn",
166
+ "health.level.skipped": "Not checked",
167
+ "health.level.fail": "fail",
168
+ "health.priority.high": "Priority",
169
+ "health.gate.generation": "Generation artifact",
170
+ "health.gate.log-integrity": "Log integrity",
171
+ "health.gate.projection-cache": "Projection cache",
172
+ "health.gate.lossless-json": "Lossless JSON",
173
+ "health.gate.cold-read": "Continuability",
174
+ "health.gate.source-kind": "Plugin signature",
175
+ "health.attribution": "Owner",
176
+ "health.field": "Field",
177
+ "health.unknownOwner": "owner unknown",
178
+ "health.phase.checkup": "1) Checkup",
179
+ "health.phase.prescribe": "2) Prescription",
180
+ "health.phase.discharge": "3) Discharge",
181
+ "health.repair": "Apply reversible repair",
182
+ "health.repairing": "Repairing…",
183
+ "health.sourceMigrate": "Migrate legacy signatures",
184
+ "health.sourceMigrating": "Migrating…",
185
+ "health.sourceMigrated": "Migrated {n} legacy signature rows (backup: {backup})",
186
+ "health.sourceMigrateNone": "No legacy signature rows to migrate",
187
+ "health.sourceMigrateFail": "Migration failed: {error}",
188
+ "health.before": "Before",
189
+ "health.after": "After",
190
+ "health.quarantined": "Projection cache quarantined (refolds after a DSH restart)",
191
+ "health.commands": "Commands",
192
+ "health.copy": "Copy",
193
+ "health.copied": "Copied",
194
+ "health.detail": "Details",
195
+ "health.back": "Back",
196
+ "health.cache.hint": "Cached result · {ago}",
197
+ "health.cache.refresh": "Refresh",
198
+ "health.cache.corpusChanged": "Corpus changed ({was} → {now}); refresh recommended",
199
+ "health.ago.justNow": "just now",
200
+ "health.ago.minutes": "{n} min ago",
201
+ "health.ago.hours": "{n} h ago",
202
+ "health.ago.days": "{n} d ago"
203
+ },
204
+ ja: {
205
+ "card.title": "セッションスチュワード",
206
+ "card.desc": "履歴ファイル · ヘルスチェック",
207
+ "card.enabled": "セッションスチュワードを有効化",
208
+ "card.enabled.desc": "オフにすると API を一切登録せず、サイドバーの入口も消えます。",
209
+ "card.history": "セッション履歴ファイル",
210
+ "card.history.desc": "アーカイブの閲覧とクリーンアップ(老人ホーム)。オフにすると履歴 API を登録せず、このタブも描画しません。",
211
+ "card.health": "ヘルスチェック",
212
+ "card.health.desc": "健診 → 処方 → 退院。オフにすると健診 API を登録せず、このタブも描画しません。",
213
+ "card.readonly": "このセッションでは設定を変更できません(読み取り専用マウント)。",
214
+ "card.unavailable": "設定サービスが利用できず、構成を読み取れません。",
215
+ "family.title": "スクリュードライバー プラグイン設定",
216
+ "panel.title": "セッションスチュワード",
217
+ "panel.tab.history": "老人ホーム",
218
+ "panel.tab.health": "健診",
219
+ "panel.close": "閉じる",
220
+ "panel.untitled": "(無題)",
221
+ "history.loading": "アーカイブ一覧を読み込んでいます…",
222
+ "history.empty": "アーカイブ集合は空です。",
223
+ "history.edit": "編集",
224
+ "history.done": "完了",
225
+ "history.selectAll": "すべて選択",
226
+ "history.unselectAll": "全選択を解除",
227
+ "history.unarchive": "アーカイブ状態を解除 ({n})",
228
+ "history.unarchiving": "アーカイブ解除中…",
229
+ "history.purge": "アーカイブファイルを消去 ({n}) · {size} 解放",
230
+ "history.purging": "消去中…",
231
+ "history.editingHint": "編集モード:セッションにチェックを入れ、二者択一 ——「アーカイブ状態を解除」は公式アーカイブ配列から除去するだけ(可逆、再起動後サイドバーに戻る);「アーカイブファイルを消去」はディスク上のトランスクリプトと投影キャッシュを実削除します(不可逆)。",
232
+ "history.restartHint": "アーカイブ配列から {removed} 件の id を除去しました(残り {remaining})。**今すぐ DSH を再起動**:ホスト終了前にアーカイブやワークスペースへの変更があると、メモリ全体のスナップショットがファイルへ書き戻され、この操作が巻き戻ります。",
233
+ "history.pendingRestart": "ホストのメモリ内にはまだ {n} 件のアーカイブが無効化されていません:一覧はストレージファイルに従って表示されます(これらは既に存在しない)が、対応するセッションは DSH を再起動するまでサイドバーに隠れたままです。",
234
+ "history.count": "全 {n} 件 · 占有 {size}",
235
+ "history.size": "トランスクリプト {t} · キャッシュ {c}",
236
+ "history.sizeUnknown": "ディスク上に実体なし",
237
+ "history.source.registry": "取得元:ホストのアーカイブレジストリ",
238
+ "history.source.storage-file": "取得元:ストレージファイル",
239
+ "history.source.none": "どちらのソースからもアーカイブ集合を読めません",
240
+ "history.confirmUnarchive": "これら {n} 件のセッションのアーカイブ状態を解除しますか?\n\n{summary}\n\nアーカイブ配列のみを編集します(自動バックアップ)、ディスク上のファイルは動きません。DSH 再起動後、これらのセッションは元のワークスペースに戻ります。",
241
+ "history.confirmPurge": "これら {n} 件のアーカイブ済みセッションのディスク上のファイルを消去しますか?\n\n{summary}\n\n合計約 {size} を解放します。\n削除対象:トランスクリプトディレクトリ(旧形式のコピーを含む)、個別の投影キャッシュ、およびアーカイブ配列とワークスペースメンバーテーブルからの除去。\n\n**この操作は不可逆で、セッションは復元できません。**",
242
+ "history.purgeResult": "{purged} 件のアーカイブ済みセッションを消去し、{size} を解放しました。完全に消失させるには DSH を再起動してください。",
243
+ "history.purgePartial": "{purged} 件を消去し、{size} を解放;{failed} 件が失敗(以下の明細参照)。",
244
+ "history.purgeFailures": "失敗の明細",
245
+ "health.scan": "健診を開始",
246
+ "health.scanning": "健診中…",
247
+ "health.progress": "{done}/{total} · 経過 {sec}s",
248
+ "health.empty": "異常なセッションは見つかりませんでした。",
249
+ "health.clean": "すべてのチェックに合格しました。",
250
+ "health.level.ok": "正常",
251
+ "health.level.warn": "注意",
252
+ "health.level.skipped": "未チェック",
253
+ "health.level.fail": "異常",
254
+ "health.priority.high": "優先",
255
+ "health.gate.generation": "世代アーティファクト",
256
+ "health.gate.log-integrity": "ログ完全性",
257
+ "health.gate.projection-cache": "投影キャッシュ",
258
+ "health.gate.lossless-json": "可逆 JSON",
259
+ "health.gate.cold-read": "継続可能性",
260
+ "health.gate.source-kind": "プラグイン署名",
261
+ "health.attribution": "帰属",
262
+ "health.field": "フィールド",
263
+ "health.unknownOwner": "帰属不明",
264
+ "health.phase.checkup": "① 健診",
265
+ "health.phase.prescribe": "② 処方",
266
+ "health.phase.discharge": "③ 退院",
267
+ "health.repair": "可逆的な処置を実行",
268
+ "health.repairing": "処置中…",
269
+ "health.sourceMigrate": "旧署名を変換",
270
+ "health.sourceMigrating": "変換中…",
271
+ "health.sourceMigrated": "{n} 行の旧署名を変換しました(バックアップ:{backup})",
272
+ "health.sourceMigrateNone": "変換すべき旧署名行はありません",
273
+ "health.sourceMigrateFail": "変換に失敗:{error}",
274
+ "health.before": "処置前",
275
+ "health.after": "処置後",
276
+ "health.quarantined": "投影キャッシュのレコードを隔離しました(DSH 再起動後に再折り畳み)",
277
+ "health.commands": "実行可能なコマンド",
278
+ "health.copy": "コピー",
279
+ "health.copied": "コピーしました",
280
+ "health.detail": "詳細を見る",
281
+ "health.back": "一覧に戻る",
282
+ "health.cache.hint": "結果はキャッシュ由来 · {ago}",
283
+ "health.cache.refresh": "更新",
284
+ "health.cache.corpusChanged": "コーパスが変化({was} → {now})、更新を推奨",
285
+ "health.ago.justNow": "たった今",
286
+ "health.ago.minutes": "{n} 分前",
287
+ "health.ago.hours": "{n} 時間前",
288
+ "health.ago.days": "{n} 日前"
289
+ },
290
+ ko: {
291
+ "card.title": "세션 스튜어드",
292
+ "card.desc": "기록 파일 · 건강 점검",
293
+ "card.enabled": "세션 스튜어드 활성화",
294
+ "card.enabled.desc": "끄면 어떤 API도 등록하지 않고 사이드바 항목이 통째로 사라집니다.",
295
+ "card.history": "세션 기록 파일",
296
+ "card.history.desc": "아카이브 탐색 및 정리(요양소). 끄면 기록 API를 등록하지 않고 이 탭을 렌더링하지 않습니다.",
297
+ "card.health": "건강 점검",
298
+ "card.health.desc": "진료 → 처방 → 퇴원. 끄면 진료 API를 등록하지 않고 이 탭을 렌더링하지 않습니다.",
299
+ "card.readonly": "이 세션에서는 설정을 수정할 수 없습니다(읽기 전용 마운트).",
300
+ "card.unavailable": "설정 서비스를 사용할 수 없어 구성을 읽을 수 없습니다.",
301
+ "family.title": "스크루드라이버 플러그인 설정",
302
+ "panel.title": "세션 스튜어드",
303
+ "panel.tab.history": "요양소",
304
+ "panel.tab.health": "진료",
305
+ "panel.close": "닫기",
306
+ "panel.untitled": "(제목 없음)",
307
+ "history.loading": "아카이브 목록을 읽는 중…",
308
+ "history.empty": "아카이브 집합이 비어 있습니다.",
309
+ "history.edit": "편집",
310
+ "history.done": "완료",
311
+ "history.selectAll": "전체 선택",
312
+ "history.unselectAll": "전체 선택 해제",
313
+ "history.unarchive": "아카이브 상태 해제 ({n})",
314
+ "history.unarchiving": "아카이브 해제 중…",
315
+ "history.purge": "아카이브 파일 정리 ({n}) · {size} 확보",
316
+ "history.purging": "정리 중…",
317
+ "history.editingHint": "편집 모드: 세션을 체크한 뒤 둘 중 하나 —— 「아카이브 상태 해제」는 공식 아카이브 배열에서만 제거(되돌릴 수 있음, 재시작 후 사이드바로 복귀); 「아카이브 파일 정리」는 디스크의 전사본과 투영 캐시를 실제 삭제(되돌릴 수 없음).",
318
+ "history.restartHint": "아카이브 배열에서 id {removed}개를 제거했습니다(남은 {remaining}). **지금 즉시 DSH를 재시작**: 호스트 종료 전 아카이브나 워크스페이스 변경이 있으면 메모리 전체 스냅샷이 파일로 다시 쓰여 이 작업이 되돌려집니다.",
319
+ "history.pendingRestart": "호스트 메모리에 아직 {n}개 아카이브가 유효합니다: 목록은 저장 파일을 따라 표시(이제 없음)하지만, 해당 세션은 DSH 재시작 전까지 사이드바에 숨겨진 채로 남아 있습니다.",
320
+ "history.count": "총 {n}건 · {size} 점유",
321
+ "history.size": "전사본 {t} · 캐시 {c}",
322
+ "history.sizeUnknown": "디스크에 실체 없음",
323
+ "history.source.registry": "출처: 호스트 아카이브 등록부",
324
+ "history.source.storage-file": "출처: 저장 파일",
325
+ "history.source.none": "두 소스 모두에서 아카이브 집합을 읽을 수 없음",
326
+ "history.confirmUnarchive": "이 {n}개 세션의 아카이브 상태를 해제하시겠습니까?\n\n{summary}\n\n아카이브 배열만 편집(자동 백업)하고 디스크 파일은 건드리지 않습니다. DSH 재시작 후 이 세션들은 원래 워크스페이스로 돌아갑니다.",
327
+ "history.confirmPurge": "이 {n}개 아카이브된 세션의 디스크 파일을 정리하시겠습니까?\n\n{summary}\n\n총 약 {size}를 확보합니다.\n삭제 대상: 전사본 디렉터리(구형식 사본 포함)와 건별 투영 캐시, 그리고 아카이브 배열 및 워크스페이스 구성원 표에서 제거.\n\n**이 작업은 되돌릴 수 없으며 세션은 복원되지 않습니다.**",
328
+ "history.purgeResult": "아카이브된 세션 {purged}개를 정리하고 {size}를 확보했습니다. 완전히 사라지게 하려면 DSH를 재시작하세요.",
329
+ "history.purgePartial": "{purged}개 정리, {size} 확보; {failed}개 실패(아래 명세 참조).",
330
+ "history.purgeFailures": "실패 명세",
331
+ "health.scan": "진료 시작",
332
+ "health.scanning": "진료 중…",
333
+ "health.progress": "{done}/{total} · {sec}s 경과",
334
+ "health.empty": "이상 세션을 찾지 못했습니다.",
335
+ "health.clean": "모든 점검을 통과했습니다.",
336
+ "health.level.ok": "정상",
337
+ "health.level.warn": "주의",
338
+ "health.level.skipped": "미점검",
339
+ "health.level.fail": "이상",
340
+ "health.priority.high": "우선",
341
+ "health.gate.generation": "세대 산출물",
342
+ "health.gate.log-integrity": "로그 무결성",
343
+ "health.gate.projection-cache": "투영 캐시",
344
+ "health.gate.lossless-json": "무손실 JSON",
345
+ "health.gate.cold-read": "연속 가능",
346
+ "health.gate.source-kind": "플러그인 서명",
347
+ "health.attribution": "귀속",
348
+ "health.field": "필드",
349
+ "health.unknownOwner": "귀속 불명",
350
+ "health.phase.checkup": "① 진료",
351
+ "health.phase.prescribe": "② 처방",
352
+ "health.phase.discharge": "③ 퇴원",
353
+ "health.repair": "가역적 처치 실행",
354
+ "health.repairing": "처치 중…",
355
+ "health.sourceMigrate": "구 서명 변환",
356
+ "health.sourceMigrating": "변환 중…",
357
+ "health.sourceMigrated": "구 서명 {n}행을 변환했습니다(백업: {backup})",
358
+ "health.sourceMigrateNone": "변환할 구 서명 행이 없습니다",
359
+ "health.sourceMigrateFail": "변환 실패: {error}",
360
+ "health.before": "처치 전",
361
+ "health.after": "처치 후",
362
+ "health.quarantined": "투영 캐시 기록을 격리했습니다(DSH 재시작 후 재접힘)",
363
+ "health.commands": "실행 가능 명령",
364
+ "health.copy": "복사",
365
+ "health.copied": "복사됨",
366
+ "health.detail": "상세 보기",
367
+ "health.back": "목록으로",
368
+ "health.cache.hint": "결과를 캐시에서 · {ago}",
369
+ "health.cache.refresh": "새로고침",
370
+ "health.cache.corpusChanged": "코퍼스 변경({was} → {now}), 새로고침 권장",
371
+ "health.ago.justNow": "방금",
372
+ "health.ago.minutes": "{n}분 전",
373
+ "health.ago.hours": "{n}시간 전",
374
+ "health.ago.days": "{n}일 전"
375
+ },
376
+ fr: {
377
+ "card.title": "Intendant de session",
378
+ "card.desc": "Fichiers d’historique · Contrôle de santé",
379
+ "card.enabled": "Activer l’intendant de session",
380
+ "card.enabled.desc": "Désactivé, aucune API n’est enregistrée et l’entrée de la barre latérale disparaît.",
381
+ "card.history": "Fichiers d’historique de session",
382
+ "card.history.desc": "Consultation et nettoyage des archives (Maison de retraite). Désactivé, aucune API d’historique n’est enregistrée et cet onglet n’est pas rendu.",
383
+ "card.health": "Contrôle de santé",
384
+ "card.health.desc": "Examen → Ordonnance → Sortie. Désactivé, aucune API de contrôle n’est enregistrée et cet onglet n’est pas rendu.",
385
+ "card.readonly": "Cette session ne peut pas modifier les réglages (montage en lecture seule).",
386
+ "card.unavailable": "Service de réglages indisponible ; configuration illisible.",
387
+ "family.title": "Réglages du plugin Screwdriver",
388
+ "panel.title": "Intendant de session",
389
+ "panel.tab.history": "Maison de retraite",
390
+ "panel.tab.health": "Examen",
391
+ "panel.close": "Fermer",
392
+ "panel.untitled": "(sans titre)",
393
+ "history.loading": "Lecture de la liste des archives…",
394
+ "history.empty": "L’ensemble des archives est vide.",
395
+ "history.edit": "Modifier",
396
+ "history.done": "Terminé",
397
+ "history.selectAll": "Tout sélectionner",
398
+ "history.unselectAll": "Tout désélectionner",
399
+ "history.unarchive": "Retirer l’état archivé ({n})",
400
+ "history.unarchiving": "Désarchivage…",
401
+ "history.purge": "Purger les fichiers archivés ({n}) · libérer {size}",
402
+ "history.purging": "Purge…",
403
+ "history.editingHint": "Mode modification : cochez des sessions puis choisissez — « Retirer l’état archivé » ne fait que les retirer du tableau officiel des archives (réversible ; elles reviennent dans la barre latérale après un redémarrage) ; « Purger les fichiers archivés » supprime réellement le transcript et le cache de projection du disque (irréversible).",
404
+ "history.restartHint": "{removed} id retirés du tableau des archives ({remaining} restants). **Redémarrez DSH maintenant** : tant que l’hôte n’est pas fermé, toute modification des archives ou de l’espace de travail réécrit l’intégralité de l’instantané en mémoire vers le fichier et annule cette opération.",
405
+ "history.pendingRestart": "{n} archives sont encore vivantes en mémoire de l’hôte : la liste suit le fichier de stockage (elles n’y sont plus), mais leurs sessions restent masquées de la barre latérale tant que DSH n’a pas redémarré.",
406
+ "history.count": "{n} entrées · {size} occupés",
407
+ "history.size": "transcript {t} · cache {c}",
408
+ "history.sizeUnknown": "aucune entité sur le disque",
409
+ "history.source.registry": "Source : registre d’archives de l’hôte",
410
+ "history.source.storage-file": "Source : fichier de stockage",
411
+ "history.source.none": "L’ensemble des archives est illisible des deux sources",
412
+ "history.confirmUnarchive": "Retirer l’état archivé de ces {n} session(s) ?\n\n{summary}\n\nSeul le tableau des archives est modifié (sauvegarde automatique) ; rien sur le disque n’est touché. Après un redémarrage de DSH, ces sessions reviennent dans leurs espaces de travail.",
413
+ "history.confirmPurge": "Purger les fichiers sur disque de ces {n} session(s) archivée(s) ?\n\n{summary}\n\nEnviron {size} seront libérés.\nSeront supprimés : le répertoire des transcripts (y compris les copies de format antérieur) et le cache de projection par session, puis retrait du tableau des archives et des tables de membres de l’espace de travail.\n\n**Cette opération est irréversible — les sessions seront irrécupérables.**",
414
+ "history.purgeResult": "{purged} session(s) archivée(s) purgée(s), {size} libérés. Redémarrez DSH pour qu’elles disparaissent complètement.",
415
+ "history.purgePartial": "{purged} purgée(s), {size} libérés ; {failed} en échec (voir le détail ci-dessous).",
416
+ "history.purgeFailures": "Détail des échecs",
417
+ "health.scan": "Lancer le contrôle",
418
+ "health.scanning": "Contrôle en cours…",
419
+ "health.progress": "{done}/{total} · {sec}s écoulés",
420
+ "health.empty": "Aucune session problématique trouvée.",
421
+ "health.clean": "Tous les contrôles sont passés.",
422
+ "health.level.ok": "ok",
423
+ "health.level.warn": "attention",
424
+ "health.level.skipped": "Non contrôlé",
425
+ "health.level.fail": "anomalie",
426
+ "health.priority.high": "Priorité",
427
+ "health.gate.generation": "Artefact de génération",
428
+ "health.gate.log-integrity": "Intégrité des journaux",
429
+ "health.gate.projection-cache": "Cache de projection",
430
+ "health.gate.lossless-json": "JSON sans perte",
431
+ "health.gate.cold-read": "Continuité",
432
+ "health.gate.source-kind": "Signature du plugin",
433
+ "health.attribution": "Propriétaire",
434
+ "health.field": "Champ",
435
+ "health.unknownOwner": "propriétaire inconnu",
436
+ "health.phase.checkup": "1) Examen",
437
+ "health.phase.prescribe": "2) Ordonnance",
438
+ "health.phase.discharge": "3) Sortie",
439
+ "health.repair": "Appliquer une réparation réversible",
440
+ "health.repairing": "Réparation…",
441
+ "health.sourceMigrate": "Convertir les signatures héritées",
442
+ "health.sourceMigrating": "Conversion…",
443
+ "health.sourceMigrated": "{n} lignes de signature héritée converties (sauvegarde : {backup})",
444
+ "health.sourceMigrateNone": "Aucune ligne de signature héritée à convertir",
445
+ "health.sourceMigrateFail": "Échec de la conversion : {error}",
446
+ "health.before": "Avant",
447
+ "health.after": "Après",
448
+ "health.quarantined": "Enregistrement du cache de projection mis en quarantaine (replié après un redémarrage de DSH)",
449
+ "health.commands": "Commandes exécutables",
450
+ "health.copy": "Copier",
451
+ "health.copied": "Copié",
452
+ "health.detail": "Voir le détail",
453
+ "health.back": "Retour à la liste",
454
+ "health.cache.hint": "Résultat issu du cache · {ago}",
455
+ "health.cache.refresh": "Rafraîchir",
456
+ "health.cache.corpusChanged": "Corpus modifié ({was} → {now}) ; rafraîchissement conseillé",
457
+ "health.ago.justNow": "à l’instant",
458
+ "health.ago.minutes": "il y a {n} min",
459
+ "health.ago.hours": "il y a {n} h",
460
+ "health.ago.days": "il y a {n} j"
461
+ },
462
+ de: {
463
+ "card.title": "Sitzungs-Verwalter",
464
+ "card.desc": "Verlaufsdateien · Health-Check",
465
+ "card.enabled": "Sitzungs-Verwalter aktivieren",
466
+ "card.enabled.desc": "Wenn aus, wird keine API registriert und der Seitenleisten-Eintrag verschwindet komplett.",
467
+ "card.history": "Sitzungsverlaufsdateien",
468
+ "card.history.desc": "Archivansicht und Bereinigung (Altersheim). Wenn aus, werden keine Verlaufs-Routen registriert und dieser Tab nicht gerendert.",
469
+ "card.health": "Health-Check",
470
+ "card.health.desc": "Untersuchung → Rezept → Entlassung. Wenn aus, werden keine Check-Routen registriert und dieser Tab nicht gerendert.",
471
+ "card.readonly": "Diese Sitzung kann Einstellungen nicht ändern (schreibgeschützter Mount).",
472
+ "card.unavailable": "Einstellungsdienst nicht verfügbar; Konfiguration kann nicht gelesen werden.",
473
+ "family.title": "Screwdriver-Plugin-Einstellungen",
474
+ "panel.title": "Sitzungs-Verwalter",
475
+ "panel.tab.history": "Altersheim",
476
+ "panel.tab.health": "Untersuchung",
477
+ "panel.close": "Schließen",
478
+ "panel.untitled": "(ohne Titel)",
479
+ "history.loading": "Archivliste wird gelesen…",
480
+ "history.empty": "Die Archivmenge ist leer.",
481
+ "history.edit": "Bearbeiten",
482
+ "history.done": "Fertig",
483
+ "history.selectAll": "Alle auswählen",
484
+ "history.unselectAll": "Auswahl aufheben",
485
+ "history.unarchive": "Archivstatus aufheben ({n})",
486
+ "history.unarchiving": "Aufheben des Archivstatus…",
487
+ "history.purge": "Archivdateien löschen ({n}) · {size} freigeben",
488
+ "history.purging": "Löschen…",
489
+ "history.editingHint": "Bearbeitungsmodus: Sitzungen anhaken, dann eine von beiden — „Archivstatus aufheben“ entfernt sie nur aus dem offiziellen Archiv-Feld (umkehrbar; nach einem Neustart zurück in der Seitenleiste); „Archivdateien löschen“ löscht Transkript und Projektions-Cache wirklich von der Disk (unumkehrbar).",
490
+ "history.restartHint": "{removed} id(s) aus dem Archiv-Feld entfernt ({remaining} verbleiben). **DSH jetzt neu starten**: Bevor der Host beendet wird, schreibt jede Archiv- oder Workspace-Änderung den kompletten Speicher-Snapshot zurück in die Datei und macht diese Operation zunichte.",
491
+ "history.pendingRestart": "Im Host-Speicher sind noch {n} Archive aktiv: Die Liste folgt der Speicherdatei (sie sind dort nicht mehr), aber ihre Sitzungen bleiben bis zu einem DSH-Neustart in der Seitenleiste verborgen.",
492
+ "history.count": "{n} Einträge · {size} belegt",
493
+ "history.size": "Transkript {t} · Cache {c}",
494
+ "history.sizeUnknown": "kein Objekt auf der Disk",
495
+ "history.source.registry": "Quelle: Host-Archivregister",
496
+ "history.source.storage-file": "Quelle: Speicherdatei",
497
+ "history.source.none": "Die Archivmenge ist aus beiden Quellen nicht lesbar",
498
+ "history.confirmUnarchive": "Den Archivstatus dieser {n} Sitzung(en) aufheben?\n\n{summary}\n\nNur das Archiv-Feld wird bearbeitet (automatische Sicherung); nichts auf der Disk wird angetastet. Nach einem DSH-Neustart kehren diese Sitzungen in ihre Workspaces zurück.",
499
+ "history.confirmPurge": "Die Dateien auf der Disk dieser {n} archivierten Sitzung(en) löschen?\n\n{summary}\n\nInsgesamt werden etwa {size} freigegeben.\nGelöscht werden: das Transkript-Verzeichnis (inkl. Kopien im Altformat) und der sitzungsweite Projektions-Cache, zudem Entfernung aus dem Archiv-Feld und den Workspace-Mitgliedstabellen.\n\n**Dieser Vorgang ist unumkehrbar — die Sitzungen sind nicht wiederherstellbar.**",
500
+ "history.purgeResult": "{purged} archivierte Sitzung(en) gelöscht, {size} freigegeben. DSH neu starten, damit sie endgültig verschwinden.",
501
+ "history.purgePartial": "{purged} gelöscht, {size} freigegeben; {failed} fehlgeschlagen (Details unten).",
502
+ "history.purgeFailures": "Fehlerdetails",
503
+ "health.scan": "Untersuchung starten",
504
+ "health.scanning": "Untersuchung läuft…",
505
+ "health.progress": "{done}/{total} · {sec}s vergangen",
506
+ "health.empty": "Keine Problem-Sitzung gefunden.",
507
+ "health.clean": "Alle Prüfungen bestanden.",
508
+ "health.level.ok": "ok",
509
+ "health.level.warn": "Achtung",
510
+ "health.level.skipped": "Nicht geprüft",
511
+ "health.level.fail": "Anomalie",
512
+ "health.priority.high": "Priorität",
513
+ "health.gate.generation": "Generations-Artefakt",
514
+ "health.gate.log-integrity": "Protokoll-Integrität",
515
+ "health.gate.projection-cache": "Projektions-Cache",
516
+ "health.gate.lossless-json": "Verlustfreies JSON",
517
+ "health.gate.cold-read": "Fortsetzbarkeit",
518
+ "health.gate.source-kind": "Plugin-Signatur",
519
+ "health.attribution": "Zugehörigkeit",
520
+ "health.field": "Feld",
521
+ "health.unknownOwner": "Zugehörigkeit unbekannt",
522
+ "health.phase.checkup": "1) Untersuchung",
523
+ "health.phase.prescribe": "2) Rezept",
524
+ "health.phase.discharge": "3) Entlassung",
525
+ "health.repair": "Umkehrbaren Eingriff ausführen",
526
+ "health.repairing": "Eingriff läuft…",
527
+ "health.sourceMigrate": "Alt-Signaturen umwandeln",
528
+ "health.sourceMigrating": "Umwandlung…",
529
+ "health.sourceMigrated": "{n} Zeilen Alt-Signaturen umgewandelt (Sicherung: {backup})",
530
+ "health.sourceMigrateNone": "Keine umzuwandelnden Alt-Signatur-Zeilen",
531
+ "health.sourceMigrateFail": "Umwandlung fehlgeschlagen: {error}",
532
+ "health.before": "Vorher",
533
+ "health.after": "Nachher",
534
+ "health.quarantined": "Projektions-Cache-Datensatz in Quarantäne (faltet nach einem DSH-Neustart neu)",
535
+ "health.commands": "Ausführbare Befehle",
536
+ "health.copy": "Kopieren",
537
+ "health.copied": "Kopiert",
538
+ "health.detail": "Details anzeigen",
539
+ "health.back": "Zurück zur Liste",
540
+ "health.cache.hint": "Ergebnis aus dem Cache · {ago}",
541
+ "health.cache.refresh": "Aktualisieren",
542
+ "health.cache.corpusChanged": "Korpus geändert ({was} → {now}); Aktualisierung empfohlen",
543
+ "health.ago.justNow": "gerade eben",
544
+ "health.ago.minutes": "vor {n} Min",
545
+ "health.ago.hours": "vor {n} Std",
546
+ "health.ago.days": "vor {n} Tg"
547
+ },
548
+ it: {
549
+ "card.title": "Amministratore di sessione",
550
+ "card.desc": "File della cronologia · Controllo di salute",
551
+ "card.enabled": "Abilita l’amministratore di sessione",
552
+ "card.enabled.desc": "Se disattivato, non registra alcuna API e la voce nella barra laterale scompare del tutto.",
553
+ "card.history": "File della cronologia delle sessioni",
554
+ "card.history.desc": "Consultazione e pulizia degli archivi (Casa di riposo). Se disattivato, non registra API di cronologia e non rende questa scheda.",
555
+ "card.health": "Controllo di salute",
556
+ "card.health.desc": "Visita → Prescrizione → Dimissione. Se disattivato, non registra API di controllo e non rende questa scheda.",
557
+ "card.readonly": "Questa sessione non può modificare le impostazioni (montaggio in sola lettura).",
558
+ "card.unavailable": "Servizio di impostazioni non disponibile; impossibile leggere la configurazione.",
559
+ "family.title": "Impostazioni del plugin Screwdriver",
560
+ "panel.title": "Amministratore di sessione",
561
+ "panel.tab.history": "Casa di riposo",
562
+ "panel.tab.health": "Visita",
563
+ "panel.close": "Chiudi",
564
+ "panel.untitled": "(senza titolo)",
565
+ "history.loading": "Lettura dell’elenco degli archivi…",
566
+ "history.empty": "L’insieme degli archivi è vuoto.",
567
+ "history.edit": "Modifica",
568
+ "history.done": "Fatto",
569
+ "history.selectAll": "Seleziona tutto",
570
+ "history.unselectAll": "Deseleziona tutto",
571
+ "history.unarchive": "Rimuovi lo stato di archivio ({n})",
572
+ "history.unarchiving": "Rimozione dell’archivio…",
573
+ "history.purge": "Elimina i file archiviati ({n}) · libera {size}",
574
+ "history.purging": "Eliminazione…",
575
+ "history.editingHint": "Modalità modifica: seleziona le sessioni, poi scegli uno dei due — «Rimuovi lo stato di archivio» le toglie solo dall’array ufficiale degli archivi (reversibile; dopo un riavvio tornano nella barra laterale); «Elimina i file archiviati» cancella davvero la trascrizione e la cache di proiezione dal disco (irreversibile).",
576
+ "history.restartHint": "Rimossi {removed} id dall’array degli archivi ({remaining} rimanenti). **Riavvia subito DSH**: finché l’host non è terminato, qualsiasi modifica ad archivi o workspace riscrive l’intera istantanea in memoria nel file e annulla l’operazione.",
577
+ "history.pendingRestart": "Nella memoria dell’host ci sono ancora {n} archivi attivi: l’elenco segue il file di archiviazione (non ci sono più), ma le loro sessioni restano nascoste nella barra laterale finché DSH non viene riavviato.",
578
+ "history.count": "{n} voci · {size} occupati",
579
+ "history.size": "trascrizione {t} · cache {c}",
580
+ "history.sizeUnknown": "nessun elemento su disco",
581
+ "history.source.registry": "Origine: registro degli archivi dell’host",
582
+ "history.source.storage-file": "Origine: file di archiviazione",
583
+ "history.source.none": "L’insieme degli archivi è illeggibile da entrambe le origini",
584
+ "history.confirmUnarchive": "Rimuovere lo stato di archivio di queste {n} sessioni?\n\n{summary}\n\nViene modificato solo l’array degli archivi (backup automatico); nulla su disco viene toccato. Dopo un riavvio di DSH queste sessioni tornano nei rispettivi workspace.",
585
+ "history.confirmPurge": "Eliminare i file su disco di queste {n} sessioni archiviate?\n\n{summary}\n\nVerranno liberati circa {size} in totale.\nVerranno eliminati: la directory delle trascrizioni (comprese le copie in formato vecchio) e la cache di proiezione per singolo elemento, e la rimozione dall’array degli archivi e dalle tabelle dei membri del workspace.\n\n**Questa operazione è irreversibile: le sessioni non saranno recuperabili.**",
586
+ "history.purgeResult": "Eliminate {purged} sessioni archiviate, liberati {size}. Riavvia DSH perché scompaiano del tutto.",
587
+ "history.purgePartial": "Eliminate {purged}, liberati {size}; {failed} fallite (vedi dettagli sotto).",
588
+ "history.purgeFailures": "Dettagli dei fallimenti",
589
+ "health.scan": "Avvia il controllo",
590
+ "health.scanning": "Controllo in corso…",
591
+ "health.progress": "{done}/{total} · {sec}s impiegati",
592
+ "health.empty": "Nessuna sessione problematica trovata.",
593
+ "health.clean": "Tutti i controlli superati.",
594
+ "health.level.ok": "ok",
595
+ "health.level.warn": "attenzione",
596
+ "health.level.skipped": "Non controllato",
597
+ "health.level.fail": "anomalia",
598
+ "health.priority.high": "Priorità",
599
+ "health.gate.generation": "Artefatto di generazione",
600
+ "health.gate.log-integrity": "Integrità dei log",
601
+ "health.gate.projection-cache": "Cache di proiezione",
602
+ "health.gate.lossless-json": "JSON senza perdita",
603
+ "health.gate.cold-read": "Continuità",
604
+ "health.gate.source-kind": "Firma del plugin",
605
+ "health.attribution": "Appartenenza",
606
+ "health.field": "Campo",
607
+ "health.unknownOwner": "appartenenza sconosciuta",
608
+ "health.phase.checkup": "1) Visita",
609
+ "health.phase.prescribe": "2) Prescrizione",
610
+ "health.phase.discharge": "3) Dimissione",
611
+ "health.repair": "Esegui intervento reversibile",
612
+ "health.repairing": "Intervento…",
613
+ "health.sourceMigrate": "Converti firme ereditate",
614
+ "health.sourceMigrating": "Conversione…",
615
+ "health.sourceMigrated": "Convertite {n} righe di firme ereditate (backup: {backup})",
616
+ "health.sourceMigrateNone": "Nessuna riga di firma ereditata da convertire",
617
+ "health.sourceMigrateFail": "Conversione fallita: {error}",
618
+ "health.before": "Prima",
619
+ "health.after": "Dopo",
620
+ "health.quarantined": "Record della cache di proiezione messo in quarantena (si ripiega dopo un riavvio di DSH)",
621
+ "health.commands": "Comandi eseguibili",
622
+ "health.copy": "Copia",
623
+ "health.copied": "Copiato",
624
+ "health.detail": "Vedi dettagli",
625
+ "health.back": "Torna all’elenco",
626
+ "health.cache.hint": "Risultato dalla cache · {ago}",
627
+ "health.cache.refresh": "Aggiorna",
628
+ "health.cache.corpusChanged": "Corpo testuale modificato ({was} → {now}); aggiornamento consigliato",
629
+ "health.ago.justNow": "proprio ora",
630
+ "health.ago.minutes": "{n} min fa",
631
+ "health.ago.hours": "{n} h fa",
632
+ "health.ago.days": "{n} g fa"
633
+ },
634
+ ru: {
635
+ "card.title": "Распорядитель сессий",
636
+ "card.desc": "Файлы истории · Проверка здоровья",
637
+ "card.enabled": "Включить распорядителя сессий",
638
+ "card.enabled.desc": "При выключении не регистрируется ни один API, и пункт боковой панели полностью исчезает.",
639
+ "card.history": "Файлы истории сессий",
640
+ "card.history.desc": "Просмотр и очистка архивов (Пансионат). При выключении не регистрируются API истории и эта вкладка не отображается.",
641
+ "card.health": "Проверка здоровья",
642
+ "card.health.desc": "Осмотр → Рецепт → Выписка. При выключении не регистрируются API проверки и эта вкладка не отображается.",
643
+ "card.readonly": "Эта сессия не может изменять настройки (монтирование только для чтения).",
644
+ "card.unavailable": "Сервис настроек недоступен; конфигурацию невозможно прочитать.",
645
+ "family.title": "Настройки плагинов Screwdriver",
646
+ "panel.title": "Распорядитель сессий",
647
+ "panel.tab.history": "Пансионат",
648
+ "panel.tab.health": "Осмотр",
649
+ "panel.close": "Закрыть",
650
+ "panel.untitled": "(без названия)",
651
+ "history.loading": "Чтение списка архивов…",
652
+ "history.empty": "Множество архивов пусто.",
653
+ "history.edit": "Изменить",
654
+ "history.done": "Готово",
655
+ "history.selectAll": "Выбрать все",
656
+ "history.unselectAll": "Снять выбор",
657
+ "history.unarchive": "Снять статус архива ({n})",
658
+ "history.unarchiving": "Снятие с архива…",
659
+ "history.purge": "Удалить файлы архива ({n}) · освободить {size}",
660
+ "history.purging": "Удаление…",
661
+ "history.editingHint": "Режим правки: отметьте сессии, затем выберите одно — «Снять статус архива» лишь убирает их из официального массива архивов (обратимо; после перезапуска вернутся в боковую панель); «Удалить файлы архива» реально стирает транскрипт и кэш проекций с диска (необратимо).",
662
+ "history.restartHint": "Из массива архивов удалено {removed} id (осталось {remaining}). **Немедленно перезапустите DSH**: пока хост не завершился, любые изменения архивов или рабочего пространства перезаписывают весь снимок из памяти обратно в файл, и эта операция будет отменена.",
663
+ "history.pendingRestart": "В памяти хоста ещё {n} архивов остаются действительными: список следует файлу хранения (их уже нет), но соответствующие сессии скрыты из боковой панели до перезапуска DSH.",
664
+ "history.count": "Всего {n} · занято {size}",
665
+ "history.size": "транскрипт {t} · кэш {c}",
666
+ "history.sizeUnknown": "на диске сущностей нет",
667
+ "history.source.registry": "Источник: реестр архивов хоста",
668
+ "history.source.storage-file": "Источник: файл хранения",
669
+ "history.source.none": "Множество архивов не читается из обоих источников",
670
+ "history.confirmUnarchive": "Снять статус архива у этих {n} сессий?\n\n{summary}\n\nРедактируется только массив архивов (автоматическое резервное копирование); файлы на диске не трогаются. После перезапуска DSH эти сессии вернутся в свои рабочие пространства.",
671
+ "history.confirmPurge": "Удалить файлы на диске у этих {n} архивированных сессий?\n\n{summary}\n\nВсего освободится около {size}.\nБудут удалены: каталог транскриптов (включая копии старого формата) и поэлементный кэш проекций, а также удаление из массива архивов и таблиц участников рабочего пространства.\n\n**Эта операция необратима — сессии невозможно будет восстановить.**",
672
+ "history.purgeResult": "Удалено {purged} архивированных сессий, освобождено {size}. Перезапустите DSH, чтобы они исчезли окончательно.",
673
+ "history.purgePartial": "Удалено {purged}, освобождено {size}; {failed} с ошибками (подробности ниже).",
674
+ "history.purgeFailures": "Подробности ошибок",
675
+ "health.scan": "Начать осмотр",
676
+ "health.scanning": "Идёт осмотр…",
677
+ "health.progress": "{done}/{total} · прошло {sec}с",
678
+ "health.empty": "Проблемных сессий не найдено.",
679
+ "health.clean": "Все проверки пройдены.",
680
+ "health.level.ok": "норма",
681
+ "health.level.warn": "внимание",
682
+ "health.level.skipped": "не проверено",
683
+ "health.level.fail": "аномалия",
684
+ "health.priority.high": "Приоритет",
685
+ "health.gate.generation": "Артефакт поколения",
686
+ "health.gate.log-integrity": "Целостность журнала",
687
+ "health.gate.projection-cache": "Кэш проекций",
688
+ "health.gate.lossless-json": "JSON без потерь",
689
+ "health.gate.cold-read": "Возможность продолжения",
690
+ "health.gate.source-kind": "Подпись плагина",
691
+ "health.attribution": "Принадлежность",
692
+ "health.field": "Поле",
693
+ "health.unknownOwner": "принадлежность неизвестна",
694
+ "health.phase.checkup": "1) Осмотр",
695
+ "health.phase.prescribe": "2) Рецепт",
696
+ "health.phase.discharge": "3) Выписка",
697
+ "health.repair": "Выполнить обратимое воздействие",
698
+ "health.repairing": "Воздействие…",
699
+ "health.sourceMigrate": "Преобразовать старые подписи",
700
+ "health.sourceMigrating": "Преобразование…",
701
+ "health.sourceMigrated": "Преобразовано {n} строк старых подписей (резервная копия: {backup})",
702
+ "health.sourceMigrateNone": "Нет строк старых подписей для преобразования",
703
+ "health.sourceMigrateFail": "Ошибка преобразования: {error}",
704
+ "health.before": "До",
705
+ "health.after": "После",
706
+ "health.quarantined": "Запись кэша проекций помещена в карантин (пересоберётся после перезапуска DSH)",
707
+ "health.commands": "Выполнимые команды",
708
+ "health.copy": "Копировать",
709
+ "health.copied": "Скопировано",
710
+ "health.detail": "Показать подробности",
711
+ "health.back": "К списку",
712
+ "health.cache.hint": "Результат из кэша · {ago}",
713
+ "health.cache.refresh": "Обновить",
714
+ "health.cache.corpusChanged": "Корпус изменился ({was} → {now}); рекомендуется обновление",
715
+ "health.ago.justNow": "только что",
716
+ "health.ago.minutes": "{n} мин назад",
717
+ "health.ago.hours": "{n} ч назад",
718
+ "health.ago.days": "{n} дн назад"
719
+ },
720
+ es: {
721
+ "card.title": "Mayordomo de sesión",
722
+ "card.desc": "Archivos de historial · Comprobación de salud",
723
+ "card.enabled": "Activar el mayordomo de sesión",
724
+ "card.enabled.desc": "Al desactivarlo no se registra ninguna API y la entrada de la barra lateral desaparece por completo.",
725
+ "card.history": "Archivos de historial de sesión",
726
+ "card.history.desc": "Visualización y limpieza del archivo (Residencia). Al desactivarlo no se registran rutas de historial ni se muestra esa pestaña.",
727
+ "card.health": "Comprobación de salud",
728
+ "card.health.desc": "Revisión → Receta → Alta. Al desactivarlo no se registran rutas de revisión ni se muestra esa pestaña.",
729
+ "card.readonly": "Esta sesión no puede modificar la configuración (montaje de solo lectura).",
730
+ "card.unavailable": "Servicio de configuración no disponible; no se puede leer la configuración.",
731
+ "family.title": "Ajustes del plugin Screwdriver",
732
+ "panel.title": "Mayordomo de sesión",
733
+ "panel.tab.history": "Residencia",
734
+ "panel.tab.health": "Revisión",
735
+ "panel.close": "Cerrar",
736
+ "panel.untitled": "(sin título)",
737
+ "history.loading": "Leyendo la lista de archivos…",
738
+ "history.empty": "El conjunto de archivos está vacío.",
739
+ "history.edit": "Editar",
740
+ "history.done": "Listo",
741
+ "history.selectAll": "Seleccionar todo",
742
+ "history.unselectAll": "Deseleccionar todo",
743
+ "history.unarchive": "Quitar el estado de archivo ({n})",
744
+ "history.unarchiving": "Quitando del archivo…",
745
+ "history.purge": "Purgar archivos del archivo ({n}) · liberar {size}",
746
+ "history.purging": "Purgando…",
747
+ "history.editingHint": "Modo edición: marca sesiones y elige una de dos — «Quitar el estado de archivo» solo las retira de la matriz oficial de archivos (reversible; vuelven a la barra lateral tras reiniciar); «Purgar archivos del archivo» borra de verdad la transcripción y la caché de proyección del disco (irreversible).",
748
+ "history.restartHint": "Se retiraron {removed} id de la matriz de archivos (quedan {remaining}). **Reinicia DSH ahora**: hasta que el host se cierre, cualquier cambio en archivos o espacio de trabajo reescribe toda la instantánea en memoria al archivo y deshace esta operación.",
749
+ "history.pendingRestart": "En la memoria del host aún hay {n} archivos vigentes: la lista sigue el archivo de almacenamiento (ya no están), pero sus sesiones quedan ocultas de la barra lateral hasta reiniciar DSH.",
750
+ "history.count": "{n} en total · {size} ocupados",
751
+ "history.size": "transcripción {t} · caché {c}",
752
+ "history.sizeUnknown": "sin entidad en el disco",
753
+ "history.source.registry": "Origen: registro de archivos del host",
754
+ "history.source.storage-file": "Origen: archivo de almacenamiento",
755
+ "history.source.none": "El conjunto de archivos no se puede leer desde ninguno de los dos orígenes",
756
+ "history.confirmUnarchive": "¿Quitar el estado de archivo de estas {n} sesiones?\n\n{summary}\n\nSolo se edita la matriz de archivos (copia de seguridad automática); no se toca nada en el disco. Tras reiniciar DSH, estas sesiones vuelven a sus espacios de trabajo.",
757
+ "history.confirmPurge": "¿Purgar los archivos en disco de estas {n} sesiones archivadas?\n\n{summary}\n\nSe liberarán unos {size} en total.\nSe borrarán: el directorio de transcripciones (incluidas las copias de formato antiguo) y la caché de proyección por elemento, y se retirarán de la matriz de archivos y de las tablas de miembros del espacio de trabajo.\n\n**Esta operación es irreversible; las sesiones no se podrán recuperar.**",
758
+ "history.purgeResult": "Purgadas {purged} sesiones archivadas, liberados {size}. Reinicia DSH para que desaparezcan por completo.",
759
+ "history.purgePartial": "Purgadas {purged}, liberados {size}; {failed} fallidas (ver detalles abajo).",
760
+ "history.purgeFailures": "Detalles de los fallos",
761
+ "health.scan": "Iniciar revisión",
762
+ "health.scanning": "Revisando…",
763
+ "health.progress": "{done}/{total} · {sec}s transcurridos",
764
+ "health.empty": "No se encontró ninguna sesión problemática.",
765
+ "health.clean": "Todas las comprobaciones pasaron.",
766
+ "health.level.ok": "correcto",
767
+ "health.level.warn": "atención",
768
+ "health.level.skipped": "No comprobado",
769
+ "health.level.fail": "anomalía",
770
+ "health.priority.high": "Prioridad",
771
+ "health.gate.generation": "Artefacto de generación",
772
+ "health.gate.log-integrity": "Integridad del registro",
773
+ "health.gate.projection-cache": "Caché de proyección",
774
+ "health.gate.lossless-json": "JSON sin pérdida",
775
+ "health.gate.cold-read": "Continuidad",
776
+ "health.gate.source-kind": "Firma del plugin",
777
+ "health.attribution": "Titular",
778
+ "health.field": "Campo",
779
+ "health.unknownOwner": "titular desconocido",
780
+ "health.phase.checkup": "1) Revisión",
781
+ "health.phase.prescribe": "2) Receta",
782
+ "health.phase.discharge": "3) Alta",
783
+ "health.repair": "Aplicar reparación reversible",
784
+ "health.repairing": "Reparando…",
785
+ "health.sourceMigrate": "Convertir firmas heredadas",
786
+ "health.sourceMigrating": "Convirtiendo…",
787
+ "health.sourceMigrated": "Convertidas {n} filas de firmas heredadas (copia de seguridad: {backup})",
788
+ "health.sourceMigrateNone": "No hay filas de firmas heredadas que convertir",
789
+ "health.sourceMigrateFail": "Conversión fallida: {error}",
790
+ "health.before": "Antes",
791
+ "health.after": "Después",
792
+ "health.quarantined": "Registro de la caché de proyección puesto en cuarentena (se repliega tras reiniciar DSH)",
793
+ "health.commands": "Comandos ejecutables",
794
+ "health.copy": "Copiar",
795
+ "health.copied": "Copiado",
796
+ "health.detail": "Ver detalles",
797
+ "health.back": "Volver a la lista",
798
+ "health.cache.hint": "Resultado desde la caché · {ago}",
799
+ "health.cache.refresh": "Actualizar",
800
+ "health.cache.corpusChanged": "El corpus cambió ({was} → {now}); se recomienda actualizar",
801
+ "health.ago.justNow": "ahora mismo",
802
+ "health.ago.minutes": "hace {n} min",
803
+ "health.ago.hours": "hace {n} h",
804
+ "health.ago.days": "hace {n} d"
805
+ }
195
806
  };
196
807
  /** 字典查找(缺 key 时回退到内置 zh,再回退 key 本身)。 */
197
808
  function translate(t, key, params) {
198
809
  if (t !== void 0) try {
199
810
  return t(key, params);
200
811
  } catch {}
201
- return format(zh[key] ?? key, params);
812
+ return format(zh$1[key] ?? key, params);
202
813
  }
203
814
  /** 极简 `{name}` 替换。 */
204
815
  function format(template, params) {
@@ -220,7 +831,7 @@ window.__ModuleLoader__.load({
220
831
  * 与同节其它抽屉一样默认展开。
221
832
  */
222
833
  /** 一行设置项。 */
223
- function Row(props) {
834
+ function Row$1(props) {
224
835
  return (0, react.createElement)("div", { className: "dss_setRow" }, [(0, react.createElement)("div", {
225
836
  key: "text",
226
837
  className: "dss_setText"
@@ -233,7 +844,7 @@ window.__ModuleLoader__.load({
233
844
  }, props.desc)]), (0, react.createElement)("div", { key: "ctl" }, props.control)]);
234
845
  }
235
846
  /** 一个布尔开关(原生 checkbox 语义,样式由本插件 CSS 提供)。 */
236
- function Toggle(props) {
847
+ function Toggle$1(props) {
237
848
  return (0, react.createElement)("label", { className: `dss_toggle${props.checked ? " dss_toggleOn" : ""}` }, [(0, react.createElement)("input", {
238
849
  key: "i",
239
850
  type: "checkbox",
@@ -292,33 +903,33 @@ window.__ModuleLoader__.load({
292
903
  className: "dss_setDesc"
293
904
  }, translate(t, "card.unavailable")));
294
905
  else {
295
- rows.push((0, react.createElement)(Row, {
906
+ rows.push((0, react.createElement)(Row$1, {
296
907
  key: "enabled",
297
908
  title: translate(t, "card.enabled"),
298
909
  desc: translate(t, "card.enabled.desc"),
299
- control: (0, react.createElement)(Toggle, {
910
+ control: (0, react.createElement)(Toggle$1, {
300
911
  checked: value.enabled ?? DEFAULT_CONFIG.enabled,
301
912
  disabled: !writable,
302
913
  onChange: (checked) => {
303
914
  scope.set("enabled", checked);
304
915
  }
305
916
  })
306
- }), (0, react.createElement)(Row, {
917
+ }), (0, react.createElement)(Row$1, {
307
918
  key: "history",
308
919
  title: translate(t, "card.history"),
309
920
  desc: translate(t, "card.history.desc"),
310
- control: (0, react.createElement)(Toggle, {
921
+ control: (0, react.createElement)(Toggle$1, {
311
922
  checked: value.historyFiles ?? DEFAULT_CONFIG.historyFiles,
312
923
  disabled: !writable,
313
924
  onChange: (checked) => {
314
925
  scope.set("historyFiles", checked);
315
926
  }
316
927
  })
317
- }), (0, react.createElement)(Row, {
928
+ }), (0, react.createElement)(Row$1, {
318
929
  key: "health",
319
930
  title: translate(t, "card.health"),
320
931
  desc: translate(t, "card.health.desc"),
321
- control: (0, react.createElement)(Toggle, {
932
+ control: (0, react.createElement)(Toggle$1, {
322
933
  checked: value.healthCheck ?? DEFAULT_CONFIG.healthCheck,
323
934
  disabled: !writable,
324
935
  onChange: (checked) => {
@@ -351,9 +962,9 @@ window.__ModuleLoader__.load({
351
962
  * 仅把路由前缀换成会话管家自己的)。
352
963
  */
353
964
  /** 单次 host 调用超时(导入类大响应可单独放宽)。 */
354
- const FETCH_TIMEOUT = 2e4;
965
+ const FETCH_TIMEOUT$1 = 2e4;
355
966
  /** POST 一个 JSON body,返回整条记录。 */
356
- function callHostAny(method, body, timeout = FETCH_TIMEOUT) {
967
+ function callHostAny$1(method, body, timeout = FETCH_TIMEOUT$1) {
357
968
  const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
358
969
  const timer = typeof setTimeout === "function" ? setTimeout(() => {
359
970
  controller?.abort();
@@ -371,6 +982,145 @@ window.__ModuleLoader__.load({
371
982
  });
372
983
  }
373
984
  //#endregion
985
+ //#region src/client/bridge-scope.ts
986
+ /** 与宿主 settings-bridge.ts 的命名空间键一致(存储键稳定承诺)。 */
987
+ const STEWARD_NS = "session-steward";
988
+ const SEARCH_NS = "switch-search";
989
+ const shared = {
990
+ steward: {},
991
+ search: {},
992
+ status: "loading",
993
+ writable: false,
994
+ revision: void 0
995
+ };
996
+ const listeners = /* @__PURE__ */ new Set();
997
+ let describeInFlight;
998
+ function notify() {
999
+ for (const listener of [...listeners]) try {
1000
+ listener();
1001
+ } catch {}
1002
+ }
1003
+ function replaceDoc(next) {
1004
+ Object.assign(shared, next);
1005
+ notify();
1006
+ }
1007
+ /** 首个订阅触发一次 describe;并发调用共享同一 in-flight promise。 */
1008
+ function ensureDescribe() {
1009
+ if (describeInFlight !== void 0) return describeInFlight;
1010
+ describeInFlight = callHostAny$1("settings-describe", {}).then((res) => {
1011
+ if (res.ok === true && res.namespaces !== null && typeof res.namespaces === "object") replaceDoc({
1012
+ steward: res.namespaces[STEWARD_NS] ?? {},
1013
+ search: res.namespaces[SEARCH_NS] ?? {},
1014
+ status: "ready",
1015
+ writable: res.writable === true,
1016
+ revision: typeof res.revision === "number" ? res.revision : void 0
1017
+ });
1018
+ else replaceDoc({
1019
+ status: "unavailable",
1020
+ writable: false
1021
+ });
1022
+ }).catch(() => {
1023
+ replaceDoc({
1024
+ status: "unavailable",
1025
+ writable: false
1026
+ });
1027
+ }).finally(() => {
1028
+ describeInFlight = void 0;
1029
+ });
1030
+ return describeInFlight;
1031
+ }
1032
+ /** 组装一个视图 scope。`ns` 决定读/写哪个命名空间文档。 */
1033
+ function makeBridgeScope(ns, keyOf) {
1034
+ let snapshot = {
1035
+ status: "loading",
1036
+ value: void 0,
1037
+ revision: void 0,
1038
+ writable: false
1039
+ };
1040
+ const refresh = () => {
1041
+ const next = {
1042
+ status: shared.status,
1043
+ value: shared.status === "ready" ? { ...keyOf(shared) } : void 0,
1044
+ revision: shared.revision,
1045
+ writable: shared.writable
1046
+ };
1047
+ if (next.status !== snapshot.status || next.writable !== snapshot.writable || next.revision !== snapshot.revision || JSON.stringify(next.value ?? null) !== JSON.stringify(snapshot.value ?? null)) snapshot = next;
1048
+ };
1049
+ return {
1050
+ getSnapshot: () => {
1051
+ refresh();
1052
+ return snapshot;
1053
+ },
1054
+ subscribe: (listener) => {
1055
+ listeners.add(listener);
1056
+ ensureDescribe();
1057
+ return () => {
1058
+ listeners.delete(listener);
1059
+ };
1060
+ },
1061
+ set: async (field, value) => {
1062
+ if (shared.status !== "ready" || shared.writable === false) throw new Error("设置桥不可写(describe 未就绪或宿主只读)");
1063
+ const before = keyOf(shared)[field];
1064
+ keyOf(shared)[field] = value;
1065
+ refresh();
1066
+ const res = await callHostAny$1("settings-mutate", {
1067
+ ns,
1068
+ fields: { [field]: value }
1069
+ });
1070
+ if (res.ok === true && res.namespaces !== null && typeof res.namespaces === "object") replaceDoc({
1071
+ steward: res.namespaces[STEWARD_NS] ?? shared.steward,
1072
+ search: res.namespaces[SEARCH_NS] ?? shared.search,
1073
+ revision: typeof res.revision === "number" ? res.revision : shared.revision
1074
+ });
1075
+ else {
1076
+ keyOf(shared)[field] = before;
1077
+ refresh();
1078
+ throw new Error(res.error ?? "设置写入失败");
1079
+ }
1080
+ }
1081
+ };
1082
+ }
1083
+ let stewardScope;
1084
+ let searchScope;
1085
+ /** 管家卡/管家入口的桥 scope(命名空间 session-steward)。 */
1086
+ function stewardBridgeScope() {
1087
+ stewardScope ??= makeBridgeScope(STEWARD_NS, (doc) => doc.steward);
1088
+ return stewardScope;
1089
+ }
1090
+ /** 搜索卡/搜索入口的桥 scope(命名空间 switch-search)。 */
1091
+ function searchBridgeScope() {
1092
+ searchScope ??= makeBridgeScope(SEARCH_NS, (doc) => doc.search);
1093
+ return searchScope;
1094
+ }
1095
+ /**
1096
+ * 原生句柄 + 桥的合成 scope:原生快照**在任意时刻**观测到 unavailable(出生
1097
+ * 死或 loading→死,0.1.7 上 steward 条目的 configForms 实测后者)就地读桥,
1098
+ * 不做 apply 期时序猜测。写侧同理:原生 unavailable 即走桥(桥在无 legacy
1099
+ * settings 服务的宿主上 set 会显式拒绝,卡片保持只读真值)。双订阅:桥的
1100
+ * describe 全页一次(模块单例),代价可忽略。
1101
+ */
1102
+ function withBridgeFallback(native, bridge) {
1103
+ if (native === void 0) return bridge;
1104
+ return {
1105
+ getSnapshot: () => {
1106
+ const snap = native.getSnapshot();
1107
+ return snap.status === "unavailable" ? bridge.getSnapshot() : snap;
1108
+ },
1109
+ subscribe: (listener) => {
1110
+ const offNative = native.subscribe(listener);
1111
+ const offBridge = bridge.subscribe(listener);
1112
+ return () => {
1113
+ offNative();
1114
+ offBridge();
1115
+ };
1116
+ },
1117
+ set: async (field, value) => {
1118
+ if (native.getSnapshot().status === "unavailable") return bridge.set(field, value);
1119
+ return native.set(field, value);
1120
+ }
1121
+ };
1122
+ }
1123
+ //#endregion
374
1124
  //#region src/client/history-panel.tsx
375
1125
  /**
376
1126
  * 养老院(会话历史文件)面板 —— 自 dsh-session-search-toggle `src/client/archive-panel.tsx`
@@ -422,7 +1172,7 @@ window.__ModuleLoader__.load({
422
1172
  (0, react.useEffect)(() => {
423
1173
  let cancelled = false;
424
1174
  setError(null);
425
- callHostAny("session-history-list", {}).then((res) => {
1175
+ callHostAny$1("session-history-list", {}).then((res) => {
426
1176
  if (cancelled) return;
427
1177
  if (res.ok === true && Array.isArray(res.items)) {
428
1178
  setItems(res.items);
@@ -484,7 +1234,7 @@ window.__ModuleLoader__.load({
484
1234
  setBusy(kind);
485
1235
  setNote(null);
486
1236
  setFailures([]);
487
- callHostAny(method, { sessionIds: rows.map((row) => row.sessionId) }, 6e4).then((res) => {
1237
+ callHostAny$1(method, { sessionIds: rows.map((row) => row.sessionId) }, 6e4).then((res) => {
488
1238
  if (res.ok === true) {
489
1239
  if (kind === "unarchive") setNote(translate(t, "history.restartHint", {
490
1240
  removed: res.removed ?? rows.length,
@@ -601,7 +1351,7 @@ window.__ModuleLoader__.load({
601
1351
  }, item.title || translate(t, "panel.untitled")), item.updatedAt > 0 && (0, react.createElement)("span", {
602
1352
  key: "tag",
603
1353
  className: "dss_tag"
604
- }, fmtTime(item.updatedAt))]),
1354
+ }, fmtTime$2(item.updatedAt))]),
605
1355
  (0, react.createElement)("span", {
606
1356
  key: "size",
607
1357
  className: "dss_meta dss_size"
@@ -667,7 +1417,7 @@ window.__ModuleLoader__.load({
667
1417
  ]);
668
1418
  }
669
1419
  /** 时间格式化(与迁移前一致)。 */
670
- function fmtTime(ms) {
1420
+ function fmtTime$2(ms) {
671
1421
  if (!ms || typeof ms !== "number") return "";
672
1422
  try {
673
1423
  const date = new Date(ms);
@@ -768,7 +1518,7 @@ window.__ModuleLoader__.load({
768
1518
  };
769
1519
  }, [onClose]);
770
1520
  (0, react.useEffect)(() => {
771
- callHostAny("session-health-scan", { resume: true }, 3e4).then((res) => {
1521
+ callHostAny$1("session-health-scan", { resume: true }, 3e4).then((res) => {
772
1522
  if (res.ok === true && res.cached === true) {
773
1523
  setFindings(res.findings ?? []);
774
1524
  setCacheInfo({
@@ -817,7 +1567,7 @@ window.__ModuleLoader__.load({
817
1567
  let offset = 0;
818
1568
  try {
819
1569
  for (;;) {
820
- const res = await callHostAny("session-health-scan", {
1570
+ const res = await callHostAny$1("session-health-scan", {
821
1571
  limit: SCAN_BATCH,
822
1572
  offset,
823
1573
  onlyProblems: true,
@@ -873,7 +1623,7 @@ window.__ModuleLoader__.load({
873
1623
  setError(null);
874
1624
  setDischarge(null);
875
1625
  setMigrateNote(null);
876
- callHostAny("session-health-session", { sessionId }, 12e4).then((res) => {
1626
+ callHostAny$1("session-health-session", { sessionId }, 12e4).then((res) => {
877
1627
  if (res.ok === true) {
878
1628
  setDetail(res);
879
1629
  if (res.report !== void 0) reconcileRow(res.report);
@@ -883,7 +1633,7 @@ window.__ModuleLoader__.load({
883
1633
  const runRepair = (sessionId) => {
884
1634
  setRepairing(true);
885
1635
  setError(null);
886
- callHostAny("session-health-repair", { sessionId }, 12e4).then((res) => {
1636
+ callHostAny$1("session-health-repair", { sessionId }, 12e4).then((res) => {
887
1637
  if (res.ok === true) {
888
1638
  setDischarge(res);
889
1639
  if (res.after !== void 0) {
@@ -907,7 +1657,7 @@ window.__ModuleLoader__.load({
907
1657
  setMigrating(true);
908
1658
  setError(null);
909
1659
  setMigrateNote(null);
910
- callHostAny("session-health-source-migrate", { sessionId }, 12e4).then((res) => {
1660
+ callHostAny$1("session-health-source-migrate", { sessionId }, 12e4).then((res) => {
911
1661
  if (res.after !== void 0) {
912
1662
  setDetail({
913
1663
  ok: true,
@@ -1166,92 +1916,1509 @@ window.__ModuleLoader__.load({
1166
1916
  return (0, react.createElement)("div", { className: "dss_tabBody" }, lines);
1167
1917
  }
1168
1918
  //#endregion
1169
- //#region src/client/panel.tsx
1919
+ //#region src/client/search/host-api.ts
1170
1920
  /**
1171
- * 会话管家面板壳:一个侧边栏入口,两个页签(养老院 / 体检)。
1921
+ * Client-side helpers and structural mirrors shared by the search panel and
1922
+ * the settings card. Everything talks to the host through the fenced
1923
+ * `/switch-search/api` route; no official package is value-imported.
1924
+ */
1925
+ /** Fetch timeout for one host call (long for import: the body can be large). */
1926
+ const FETCH_TIMEOUT = 1e4;
1927
+ /** POST a JSON body to a fenced switch-search API method (items shape). */
1928
+ function callHost(method, body) {
1929
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1930
+ const timer = controller !== void 0 && typeof setTimeout === "function" ? setTimeout(() => {
1931
+ controller.abort();
1932
+ }, FETCH_TIMEOUT) : void 0;
1933
+ return fetch(`/switch-search/api/${method}`, {
1934
+ method: "POST",
1935
+ headers: { "content-type": "application/json" },
1936
+ body: JSON.stringify(body),
1937
+ signal: controller?.signal
1938
+ }).then((res) => res.ok ? res.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${res.status}`))).then((data) => {
1939
+ const record = data;
1940
+ if (record && record.ok === true && Array.isArray(record.items)) return {
1941
+ ok: true,
1942
+ items: record.items
1943
+ };
1944
+ return {
1945
+ ok: false,
1946
+ items: [],
1947
+ error: record?.error ?? "请求失败"
1948
+ };
1949
+ }).catch((err) => ({
1950
+ ok: false,
1951
+ items: [],
1952
+ error: err instanceof DOMException && err.name === "AbortError" ? "请求超时" : String(err instanceof Error ? err.message : err)
1953
+ })).finally(() => {
1954
+ if (timer !== void 0) clearTimeout(timer);
1955
+ });
1956
+ }
1957
+ /** POST a body to a fenced switch-search API method, returning the whole record. */
1958
+ function callHostAny(method, body, timeout = FETCH_TIMEOUT) {
1959
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1960
+ const timer = typeof setTimeout === "function" ? setTimeout(() => {
1961
+ controller?.abort();
1962
+ }, timeout) : void 0;
1963
+ return fetch(`/switch-search/api/${method}`, {
1964
+ method: "POST",
1965
+ headers: { "content-type": "application/json" },
1966
+ body: typeof body === "string" ? body : JSON.stringify(body),
1967
+ signal: controller?.signal
1968
+ }).then((res) => res.ok ? res.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${res.status}`))).catch((err) => ({
1969
+ ok: false,
1970
+ error: err instanceof DOMException && err.name === "AbortError" ? "请求超时" : String(err instanceof Error ? err.message : err)
1971
+ })).finally(() => {
1972
+ if (timer !== void 0) clearTimeout(timer);
1973
+ });
1974
+ }
1975
+ /**
1976
+ * POST to the STEWARD fenced route (`/session-steward/api`) — the batch
1977
+ * management console (archive / unarchive / purge) rides the steward subdomain,
1978
+ * per the merged package's route contract (session-* methods stay there).
1979
+ */
1980
+ function callSteward(method, body, timeout = FETCH_TIMEOUT) {
1981
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1982
+ const timer = typeof setTimeout === "function" ? setTimeout(() => {
1983
+ controller?.abort();
1984
+ }, timeout) : void 0;
1985
+ return fetch(`/session-steward/api/${method}`, {
1986
+ method: "POST",
1987
+ headers: { "content-type": "application/json" },
1988
+ body: JSON.stringify(body),
1989
+ signal: controller?.signal
1990
+ }).then((res) => res.ok ? res.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${res.status}`))).catch((err) => ({
1991
+ ok: false,
1992
+ error: err instanceof DOMException && err.name === "AbortError" ? "请求超时" : String(err instanceof Error ? err.message : err)
1993
+ })).finally(() => {
1994
+ if (timer !== void 0) clearTimeout(timer);
1995
+ });
1996
+ }
1997
+ /** Trigger a browser download of the index snapshot from the host route. */
1998
+ async function downloadSnapshot() {
1999
+ const res = await fetch("/switch-search/api/index-export", { method: "POST" });
2000
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2001
+ const blob = await res.blob();
2002
+ const url = URL.createObjectURL(blob);
2003
+ const anchor = document.createElement("a");
2004
+ anchor.href = url;
2005
+ anchor.download = `switch-search-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`;
2006
+ anchor.click();
2007
+ URL.revokeObjectURL(url);
2008
+ }
2009
+ /**
2010
+ * Open a session from a search hit through whichever face the running host
2011
+ * offers: `uiWorkspace.openSession` first (0.1.7+), the pre-0.1.7
2012
+ * `sessions.open` as fallback. Neither present → silent no-op: a host line
2013
+ * this package does not target must not crash the panel.
1172
2014
  *
1173
- * 页签可见性由开关决定:`historyFiles=false` 时「养老院」消失,`healthCheck=false`
1174
- * 时「体检」消失;两者都关时进入面板直接显示空壳提示(客户端也会隐藏入口)。
2015
+ * The service names are resolved through `get` at call time, not captured at
2016
+ * apply time: this plugin applies before the session-controller / ui-workspace
2017
+ * client modules in the load order, so an eager lookup captures `undefined`
2018
+ * and every result click silently no-ops. Both are root-context singletons,
2019
+ * so by the time a user clicks a hit they are always mounted.
1175
2020
  */
1176
- /** 页签 id(导出供测试断言)。 */
1177
- const TAB_HISTORY = "dss-tab-history";
1178
- const TAB_HEALTH = "dss-tab-health";
1179
- /** 会话管家对话框。 */
1180
- function StewardPanel({ t, historyFiles, healthCheck, onClose }) {
1181
- const [tab, setTab] = (0, react.useState)(historyFiles ? "history" : "health");
1182
- (0, react.useEffect)(() => {
1183
- if (tab === "history" && !historyFiles && healthCheck) setTab("health");
1184
- if (tab === "health" && !healthCheck && historyFiles) setTab("history");
1185
- }, [
1186
- tab,
1187
- historyFiles,
1188
- healthCheck
1189
- ]);
2021
+ function openSessionThrough(get, sessionId) {
2022
+ const uiWorkspace = get("uiWorkspace");
2023
+ if (uiWorkspace !== void 0 && typeof uiWorkspace.openSession === "function") {
2024
+ uiWorkspace.openSession(sessionId);
2025
+ return;
2026
+ }
2027
+ const sessions = get("sessions");
2028
+ if (sessions !== void 0 && typeof sessions.open === "function") sessions.open(sessionId);
2029
+ }
2030
+ //#endregion
2031
+ //#region src/client/search/locales.ts
2032
+ /** `switch-search` client dictionaries (zh / en / ja / ko / fr / de / it / ru / es), thinking-levels pattern. */
2033
+ /** Dictionary namespace owned by this plugin (the host settings namespace). */
2034
+ const NS$1 = "switch-search";
2035
+ /** All shipped dictionaries by locale id. */
2036
+ const dictionaries = {
2037
+ zh: {
2038
+ "card.title": "搜索索引",
2039
+ "card.description": "侧边栏会话搜索增强:标题搜索与内容搜索一键切换。内容搜索使用插件自建索引,不依赖 DSH 官方全文索引。",
2040
+ "card.unavailable": "设置命名空间不可用:请确认插件已装配进 profile。",
2041
+ "card.readonly": "只读",
2042
+ "card.enabled": "启用会话搜索",
2043
+ "card.enabled.desc": "在侧边栏底部显示\"搜索\"入口。",
2044
+ "card.defaultMode": "默认搜索模式",
2045
+ "card.defaultMode.desc": "面板打开时默认进入标题搜索还是内容搜索。",
2046
+ "card.mode.title": "标题",
2047
+ "card.mode.content": "内容",
2048
+ "card.autoSync": "自动同步索引",
2049
+ "card.autoSync.desc": "后台按水位增量同步会话日志到独立索引。",
2050
+ "card.syncInterval": "同步间隔(秒)",
2051
+ "card.syncInterval.desc": "两次增量同步之间的最小间隔。",
2052
+ "card.archiveKeep": "归档保留份数",
2053
+ "card.archiveKeep.desc": "每次整理索引后保留的历史索引文件数量。",
2054
+ "card.index": "内容搜索索引",
2055
+ "card.index.desc": "索引正常:已收录 {indexed} 个会话。",
2056
+ "card.index.archives": ",归档 {archives} 份。",
2057
+ "card.index.empty": "独立索引尚未建立。点击\"整理索引\"从会话日志全量建立。",
2058
+ "card.index.reading": "正在读取索引状态…",
2059
+ "card.index.rebuilding": "正在整理索引… {done}/{total}(整理期间旧索引仍可搜索)",
2060
+ "card.index.syncing": "正在同步索引…",
2061
+ "card.index.failures": "{count} 个会话同步失败(详见 Host 日志)。",
2062
+ "card.index.rebuildError": "整理失败:{error}",
2063
+ "card.index.rebuild": "整理索引",
2064
+ "card.index.rebuilding.btn": "整理中…",
2065
+ "card.index.rebuild.hint": "非破坏性:构建期间旧索引继续可搜索,完成后原子切换并归档旧索引。",
2066
+ "card.index.archivedOwner": "归档会话的浏览与清理由「会话管家」(dsh-manage-session)负责。本插件只读归档集合,把已归档会话排除出索引。",
2067
+ "card.index.archivedMissing": "归档会话的浏览与清理需要「会话管家」(dsh-manage-session),当前未安装。本插件只读归档集合,把已归档会话排除出索引。",
2068
+ "card.index.export": "导出快照",
2069
+ "card.index.import": "导入快照",
2070
+ "card.index.exported": "快照已导出为下载文件。",
2071
+ "card.index.import.started": "快照导入已开始:正在后台重建索引,完成后自动切换。",
2072
+ "card.index.importParse": "快照解析失败或无有效会话。",
2073
+ "card.action.failed": "操作失败:{error}",
2074
+ "panel.titleSearch": "标题",
2075
+ "panel.contentSearch": "内容",
2076
+ "panel.searchTitle": "搜索会话标题…",
2077
+ "panel.searchContent": "搜索会话内容…",
2078
+ "panel.entry": "搜索",
2079
+ "panel.buildIndex": "建立索引",
2080
+ "panel.archived": "已排除 {count} 个已归档会话",
2081
+ "panel.rebuilding": "正在整理索引… {done}/{total}(整理期间旧索引仍可搜索)",
2082
+ "panel.unavailable": "独立索引服务不可用:Host 未完成初始化。",
2083
+ "panel.notBuilt": "独立索引尚未建立:先建立索引即可启用内容搜索(不依赖 DSH 官方全文索引)。",
2084
+ "panel.loadingSessions": "正在读取会话列表…",
2085
+ "panel.sessionsError": "读取会话列表失败:{error}",
2086
+ "panel.noSessions": "暂无会话",
2087
+ "panel.noMatch": "没有匹配的会话。",
2088
+ "panel.loadingContent": "正在搜索会话内容…",
2089
+ "panel.contentError": "内容搜索失败:{error}",
2090
+ "panel.noContent": "没有匹配的内容。",
2091
+ "panel.contentHint": "输入内容关键词开始搜索。",
2092
+ "panel.noText": "(无文本)",
2093
+ "panel.untitled": "(未命名)",
2094
+ "panel.openSession": "打开会话",
2095
+ "panel.footer.invoke": "唤出",
2096
+ "panel.footer.close": "关闭",
2097
+ "filter.all": "全部",
2098
+ "filter.user": "用户",
2099
+ "filter.reply": "回复",
2100
+ "filter.tool": "工具",
2101
+ "sort.label": "结果排序",
2102
+ "sort.relevance": "相关度",
2103
+ "sort.time": "时间",
2104
+ "sort.relevance.hint": "按匹配强度排序(默认)。",
2105
+ "sort.time.hint": "按会话最后活动时间倒序,更新的排前面;同一时间再按匹配强度。",
2106
+ "type.user/message": "用户",
2107
+ "type.assistant/message": "回复",
2108
+ "type.tool/call": "工具调用",
2109
+ "type.tool/result": "工具结果",
2110
+ "panel.manage": "管理",
2111
+ "domain.all": "全部",
2112
+ "domain.active": "活跃",
2113
+ "domain.archived": "归档",
2114
+ "tag.archived": "归档",
2115
+ "manage.hint": "按工作区分组,勾选后批量操作;删除不可撤销。",
2116
+ "manage.restartHint": "优先走宿主内存 API 即时生效(0.1.7+ 归档/取消归档免重启);不可用时直写存储文件,重启后侧栏生效。",
2117
+ "manage.done.restart": "需重启 DSH 后在侧栏完全生效。",
2118
+ "manage.group.nocwd": "(未分组)",
2119
+ "manage.selectGroup": "全选/取消本组",
2120
+ "manage.selected": "已选 {n}",
2121
+ "manage.batch.archive": "归档",
2122
+ "manage.batch.unarchive": "取消归档",
2123
+ "manage.batch.delete": "删除",
2124
+ "manage.batch.confirm": "确认删除",
2125
+ "manage.batch.working": "执行中…",
2126
+ "manage.done.archive": "已归档 {n} 个会话。",
2127
+ "manage.done.unarchive": "已取消归档 {n} 个会话。",
2128
+ "manage.done.purge": "已清理 {n} 个会话。",
2129
+ "manage.done.error": "操作失败:{error}",
2130
+ "panel.index.none": "索引未建立",
2131
+ "panel.index.count": "索引 {n}",
2132
+ "panel.index.rebuilding": "整理中 {done}/{total}",
2133
+ "panel.index.rebuild": "整理",
2134
+ "filter.favorites": "收藏",
2135
+ "date.from": "开始日期",
2136
+ "date.to": "结束日期",
2137
+ "star.on": "取消收藏",
2138
+ "star.off": "收藏",
2139
+ "workspace.label": "工作区",
2140
+ "workspace.more": "还有 {n} 个",
2141
+ "workspace.less": "收起",
2142
+ "manage.search": "搜索会话…",
2143
+ "group.collapse": "收拢/展开本组",
2144
+ "group.expandAll": "全部展开",
2145
+ "group.collapseAll": "全部收拢"
2146
+ },
2147
+ en: {
2148
+ "card.title": "Search Index",
2149
+ "card.description": "Sidebar session search with title/content mode switching. Content search uses the plugin-owned index and never depends on the official DSH full-text index.",
2150
+ "card.unavailable": "Settings namespace unavailable: make sure the plugin is assembled into the profile.",
2151
+ "card.readonly": "Read-only",
2152
+ "card.enabled": "Enable session search",
2153
+ "card.enabled.desc": "Show the \"Search\" entry at the bottom of the sidebar.",
2154
+ "card.defaultMode": "Default search mode",
2155
+ "card.defaultMode.desc": "Which mode the panel opens in.",
2156
+ "card.mode.title": "Title",
2157
+ "card.mode.content": "Content",
2158
+ "card.autoSync": "Auto sync index",
2159
+ "card.autoSync.desc": "Incrementally sync session logs into the independent index in the background.",
2160
+ "card.syncInterval": "Sync interval (seconds)",
2161
+ "card.syncInterval.desc": "Minimum interval between two incremental syncs.",
2162
+ "card.archiveKeep": "Archives to keep",
2163
+ "card.archiveKeep.desc": "How many archived index files each rebuild retains.",
2164
+ "card.index": "Content search index",
2165
+ "card.index.desc": "Index ready: {indexed} sessions collected.",
2166
+ "card.index.archives": ", {archives} archive(s).",
2167
+ "card.index.empty": "Independent index not built yet. Click \"Rebuild index\" to build it from session logs.",
2168
+ "card.index.reading": "Reading index status…",
2169
+ "card.index.rebuilding": "Rebuilding index… {done}/{total} (the old index keeps serving during the rebuild)",
2170
+ "card.index.syncing": "Syncing index…",
2171
+ "card.index.failures": "{count} session(s) failed to sync (see Host logs).",
2172
+ "card.index.rebuildError": "Rebuild failed: {error}",
2173
+ "card.index.rebuild": "Rebuild index",
2174
+ "card.index.rebuilding.btn": "Rebuilding…",
2175
+ "card.index.rebuild.hint": "Non-destructive: the old index keeps serving while the shadow builds; the swap is atomic and the old index is archived.",
2176
+ "card.index.archivedOwner": "Browsing and disposing of archived sessions belongs to dsh-manage-session. This plugin only reads the archive set and excludes those sessions from the index.",
2177
+ "card.index.archivedMissing": "Browsing and disposing of archived sessions needs dsh-manage-session, which is not installed. This plugin only reads the archive set and excludes those sessions from the index.",
2178
+ "card.index.export": "Export snapshot",
2179
+ "card.index.import": "Import snapshot",
2180
+ "card.index.exported": "Snapshot downloaded.",
2181
+ "card.index.import.started": "Import started: the index rebuilds in the background and swaps in when done.",
2182
+ "card.index.importParse": "Snapshot parse failed or contains no valid session.",
2183
+ "card.action.failed": "Action failed: {error}",
2184
+ "panel.titleSearch": "Title",
2185
+ "panel.contentSearch": "Content",
2186
+ "panel.searchTitle": "Search session titles…",
2187
+ "panel.searchContent": "Search session content…",
2188
+ "panel.entry": "Search",
2189
+ "panel.buildIndex": "Build index",
2190
+ "panel.archived": "{count} archived session(s) excluded",
2191
+ "panel.rebuilding": "Rebuilding index… {done}/{total} (the old index keeps serving)",
2192
+ "panel.unavailable": "Independent index service unavailable: Host not initialized.",
2193
+ "panel.notBuilt": "Independent index not built yet: build it once to enable content search (no official FTS index needed).",
2194
+ "panel.loadingSessions": "Loading sessions…",
2195
+ "panel.sessionsError": "Failed to load sessions: {error}",
2196
+ "panel.noSessions": "No sessions",
2197
+ "panel.noMatch": "No matching sessions.",
2198
+ "panel.loadingContent": "Searching content…",
2199
+ "panel.contentError": "Content search failed: {error}",
2200
+ "panel.noContent": "No matching content.",
2201
+ "panel.contentHint": "Type keywords to search content.",
2202
+ "panel.noText": "(no text)",
2203
+ "panel.untitled": "(untitled)",
2204
+ "panel.openSession": "Open session",
2205
+ "panel.footer.invoke": "Open",
2206
+ "panel.footer.close": "Close",
2207
+ "filter.all": "All",
2208
+ "filter.user": "User",
2209
+ "filter.reply": "Reply",
2210
+ "filter.tool": "Tool",
2211
+ "sort.label": "Result ordering",
2212
+ "sort.relevance": "Relevance",
2213
+ "sort.time": "Time",
2214
+ "sort.relevance.hint": "Order by match strength (default).",
2215
+ "sort.time.hint": "Order by session last-activity, newest first; ties break on match strength.",
2216
+ "type.user/message": "User",
2217
+ "type.assistant/message": "Reply",
2218
+ "type.tool/call": "Tool call",
2219
+ "type.tool/result": "Tool result",
2220
+ "panel.manage": "Manage",
2221
+ "domain.all": "All",
2222
+ "domain.active": "Active",
2223
+ "domain.archived": "Archived",
2224
+ "tag.archived": "Archived",
2225
+ "manage.hint": "Grouped by workspace; tick rows then batch-operate. Deletion is irreversible.",
2226
+ "manage.restartHint": "Prefers host in-memory APIs (0.1.7+ archive/unarchive apply instantly); falls back to direct storage-file edits, which need a restart.",
2227
+ "manage.done.restart": "Restart DSH for the sidebar to fully reflect this.",
2228
+ "manage.group.nocwd": "(no workspace)",
2229
+ "manage.selectGroup": "Toggle the whole group",
2230
+ "manage.selected": "{n} selected",
2231
+ "manage.batch.archive": "Archive",
2232
+ "manage.batch.unarchive": "Unarchive",
2233
+ "manage.batch.delete": "Delete",
2234
+ "manage.batch.confirm": "Confirm delete",
2235
+ "manage.batch.working": "Working…",
2236
+ "manage.done.archive": "Archived {n} sessions.",
2237
+ "manage.done.unarchive": "Unarchived {n} sessions.",
2238
+ "manage.done.purge": "Purged {n} sessions.",
2239
+ "manage.done.error": "Operation failed: {error}",
2240
+ "panel.index.none": "Index not built",
2241
+ "panel.index.count": "Index {n}",
2242
+ "panel.index.rebuilding": "Rebuilding {done}/{total}",
2243
+ "panel.index.rebuild": "Rebuild",
2244
+ "filter.favorites": "Favorites",
2245
+ "date.from": "From date",
2246
+ "date.to": "To date",
2247
+ "star.on": "Unfavorite",
2248
+ "star.off": "Favorite",
2249
+ "workspace.label": "Workspace",
2250
+ "workspace.more": "{n} more",
2251
+ "workspace.less": "Collapse",
2252
+ "manage.search": "Search sessions…",
2253
+ "group.collapse": "Collapse/expand group",
2254
+ "group.expandAll": "Expand all",
2255
+ "group.collapseAll": "Collapse all"
2256
+ },
2257
+ ja: {
2258
+ "card.title": "検索インデックス",
2259
+ "card.description": "サイドバーセッション検索の強化:タイトル検索とコンテンツ検索のワンクリック切り替え。コンテンツ検索はプラグイン独自のインデックスを使用し、DSH公式全文インデックスに依存しません。",
2260
+ "card.unavailable": "設定名前空間が利用できません:プラグインがプロファイルに組み込まれていることを確認してください。",
2261
+ "card.readonly": "読み取り専用",
2262
+ "card.enabled": "セッション検索を有効化",
2263
+ "card.enabled.desc": "サイドバー下部に「検索」エントリを表示します。",
2264
+ "card.defaultMode": "デフォルト検索モード",
2265
+ "card.defaultMode.desc": "パネルを開いたときの初期モード。",
2266
+ "card.mode.title": "タイトル",
2267
+ "card.mode.content": "コンテンツ",
2268
+ "card.autoSync": "インデックス自動同期",
2269
+ "card.autoSync.desc": "バックグラウンドでセッションログを独立インデックスに増分同期します。",
2270
+ "card.syncInterval": "同期間隔(秒)",
2271
+ "card.syncInterval.desc": "2回の増分同期間の最小間隔。",
2272
+ "card.archiveKeep": "アーカイブ保持数",
2273
+ "card.archiveKeep.desc": "インデックス再構築ごとに保持するアーカイブファイル数。",
2274
+ "card.index": "コンテンツ検索インデックス",
2275
+ "card.index.desc": "インデックス正常:{indexed} セッションを収容済み。",
2276
+ "card.index.archives": "、アーカイブ {archives} 件。",
2277
+ "card.index.empty": "独立インデックスが未構築です。「インデックス再構築」をクリックしてセッションログから構築してください。",
2278
+ "card.index.reading": "インデックス状態を読み取り中…",
2279
+ "card.index.rebuilding": "インデックス再構築中… {done}/{total}(再構築中も旧インデックスで検索可能)",
2280
+ "card.index.syncing": "インデックス同期中…",
2281
+ "card.index.failures": "{count} 件のセッション同期に失敗(Hostログ参照)。",
2282
+ "card.index.rebuildError": "再構築失敗:{error}",
2283
+ "card.index.rebuild": "インデックス再構築",
2284
+ "card.index.rebuilding.btn": "再構築中…",
2285
+ "card.index.rebuild.hint": "非破壊的:構築中も旧インデックスで検索可能、完了後にアトミック切替し旧インデックスをアーカイブ。",
2286
+ "card.index.archivedOwner": "アーカイブ済みセッションの閲覧と清理は「セッション管理者」(dsh-manage-session)が担当。本プラグインはアーカイブ集合を読み取り専用で参照し、アーカイブ済みセッションをインデックスから除外します。",
2287
+ "card.index.archivedMissing": "アーカイブ済みセッションの閲覧と清理には「セッション管理者」(dsh-manage-session)が必要ですが、現在未インストールです。本プラグインはアーカイブ集合を読み取り専用で参照し、アーカイブ済みセッションをインデックスから除外します。",
2288
+ "card.index.export": "スナップショット書出",
2289
+ "card.index.import": "スナップショット読込",
2290
+ "card.index.exported": "スナップショットをダウンロードしました。",
2291
+ "card.index.import.started": "スナップショットのインポートを開始:バックグラウンドでインデックスを再構築し、完了後に自動切替。",
2292
+ "card.index.importParse": "スナップショットの解析に失敗、または有効なセッションがありません。",
2293
+ "card.action.failed": "操作失敗:{error}",
2294
+ "panel.titleSearch": "タイトル",
2295
+ "panel.contentSearch": "コンテンツ",
2296
+ "panel.searchTitle": "セッションタイトルを検索…",
2297
+ "panel.searchContent": "セッション内容を検索…",
2298
+ "panel.entry": "検索",
2299
+ "panel.buildIndex": "インデックス構築",
2300
+ "panel.archived": "アーカイブ済み {count} 件を除外",
2301
+ "panel.rebuilding": "インデックス再構築中… {done}/{total}(再構築中も旧インデックスで検索可能)",
2302
+ "panel.unavailable": "独立インデックスサービス利用不可:Host初期化未完了。",
2303
+ "panel.notBuilt": "独立インデックス未構築:構築すればコンテンツ検索が有効になります(DSH公式全文インデックス不要)。",
2304
+ "panel.loadingSessions": "セッション一覧を読み込み中…",
2305
+ "panel.sessionsError": "セッション一覧の読み込み失敗:{error}",
2306
+ "panel.noSessions": "セッションなし",
2307
+ "panel.noMatch": "一致するセッションがありません。",
2308
+ "panel.loadingContent": "セッション内容を検索中…",
2309
+ "panel.contentError": "コンテンツ検索失敗:{error}",
2310
+ "panel.noContent": "一致するコンテンツがありません。",
2311
+ "panel.contentHint": "キーワードを入力してコンテンツを検索。",
2312
+ "panel.noText": "(テキストなし)",
2313
+ "panel.untitled": "(無題)",
2314
+ "panel.openSession": "セッションを開く",
2315
+ "panel.footer.invoke": "開く",
2316
+ "panel.footer.close": "閉じる",
2317
+ "filter.all": "すべて",
2318
+ "filter.user": "ユーザー",
2319
+ "filter.reply": "返信",
2320
+ "filter.tool": "ツール",
2321
+ "sort.label": "結果の並べ替え",
2322
+ "sort.relevance": "関連度",
2323
+ "sort.time": "時間",
2324
+ "sort.relevance.hint": "一致強度順に並べ替え(デフォルト)。",
2325
+ "sort.time.hint": "セッション最終活動日時の降順、同着時は一致強度順。",
2326
+ "type.user/message": "ユーザー",
2327
+ "type.assistant/message": "返信",
2328
+ "type.tool/call": "ツール呼び出し",
2329
+ "type.tool/result": "ツール結果"
2330
+ },
2331
+ ko: {
2332
+ "card.title": "검색 인덱스",
2333
+ "card.description": "사이드바 세션 검색 강화: 제목 검색과 콘텐츠 검색 간 원클릭 전환. 콘텐츠 검색은 플러그인 자체 인덱스를 사용하며 DSH 공식 전문 인덱스에 의존하지 않습니다.",
2334
+ "card.unavailable": "설정 네임스페이스를 사용할 수 없습니다: 플러그인이 프로파일에 조립되어 있는지 확인하세요.",
2335
+ "card.readonly": "읽기 전용",
2336
+ "card.enabled": "세션 검색 활성화",
2337
+ "card.enabled.desc": "사이드바 하단에 \"검색\" 항목을 표시합니다.",
2338
+ "card.defaultMode": "기본 검색 모드",
2339
+ "card.defaultMode.desc": "패널을 열 때 기본 모드.",
2340
+ "card.mode.title": "제목",
2341
+ "card.mode.content": "콘텐츠",
2342
+ "card.autoSync": "인덱스 자동 동기화",
2343
+ "card.autoSync.desc": "백그라운드에서 세션 로그를 독립 인덱스에 증분 동기화합니다.",
2344
+ "card.syncInterval": "동기화 간격(초)",
2345
+ "card.syncInterval.desc": "두 증분 동기화 사이의 최소 간격.",
2346
+ "card.archiveKeep": "아카이브 보존 수",
2347
+ "card.archiveKeep.desc": "인덱스 재구축 시 보존할 아카이브 파일 수.",
2348
+ "card.index": "콘텐츠 검색 인덱스",
2349
+ "card.index.desc": "인덱스 정상: {indexed}개 세션 수집 완료.",
2350
+ "card.index.archives": ", 아카이브 {archives}건.",
2351
+ "card.index.empty": "독립 인덱스가 아직 구축되지 않았습니다. \"인덱스 재구축\"을 클릭하여 세션 로그에서 구축하세요.",
2352
+ "card.index.reading": "인덱스 상태 읽는 중…",
2353
+ "card.index.rebuilding": "인덱스 재구축 중… {done}/{total} (재구축 중에도 이전 인덱스로 검색 가능)",
2354
+ "card.index.syncing": "인덱스 동기화 중…",
2355
+ "card.index.failures": "{count}개 세션 동기화 실패 (Host 로그 참조).",
2356
+ "card.index.rebuildError": "재구축 실패: {error}",
2357
+ "card.index.rebuild": "인덱스 재구축",
2358
+ "card.index.rebuilding.btn": "재구축 중…",
2359
+ "card.index.rebuild.hint": "비파괴적: 구축 중에도 이전 인덱스로 검색 가능하며, 완료 후 원자적 전환하고 이전 인덱스를 아카이브합니다.",
2360
+ "card.index.archivedOwner": "아카이브된 세션의 탐색 및 정리는 \"세션 관리자\"(dsh-manage-session)가 담당합니다. 본 플러그인은 아카이브 세트를 읽기 전용으로 참조하여 아카이브된 세션을 인덱스에서 제외합니다.",
2361
+ "card.index.archivedMissing": "아카이브된 세션의 탐색 및 정리에는 \"세션 관리자\"(dsh-manage-session)가 필요하지만 현재 미설치 상태입니다. 본 플러그인은 아카이브 세트를 읽기 전용으로 참조하여 아카이브된 세션을 인덱스에서 제외합니다.",
2362
+ "card.index.export": "스냅샷 내보내기",
2363
+ "card.index.import": "스냅샷 가져오기",
2364
+ "card.index.exported": "스냅샷이 다운로드되었습니다.",
2365
+ "card.index.import.started": "스냅샷 가져오기 시작: 백그라운드에서 인덱스를 재구축하며 완료 후 자동 전환됩니다.",
2366
+ "card.index.importParse": "스냅샷 파싱 실패 또는 유효한 세션 없음.",
2367
+ "card.action.failed": "작업 실패: {error}",
2368
+ "panel.titleSearch": "제목",
2369
+ "panel.contentSearch": "콘텐츠",
2370
+ "panel.searchTitle": "세션 제목 검색…",
2371
+ "panel.searchContent": "세션 콘텐츠 검색…",
2372
+ "panel.entry": "검색",
2373
+ "panel.buildIndex": "인덱스 구축",
2374
+ "panel.archived": "아카이브된 {count}개 세션 제외됨",
2375
+ "panel.rebuilding": "인덱스 재구축 중… {done}/{total} (재구축 중에도 이전 인덱스로 검색 가능)",
2376
+ "panel.unavailable": "독립 인덱스 서비스 사용 불가: Host 초기화 미완료.",
2377
+ "panel.notBuilt": "독립 인덱스 미구축: 구축하면 콘텐츠 검색이 활성화됩니다 (DSH 공식 전문 인덱스 불필요).",
2378
+ "panel.loadingSessions": "세션 목록 읽는 중…",
2379
+ "panel.sessionsError": "세션 목록 읽기 실패: {error}",
2380
+ "panel.noSessions": "세션 없음",
2381
+ "panel.noMatch": "일치하는 세션이 없습니다.",
2382
+ "panel.loadingContent": "세션 콘텐츠 검색 중…",
2383
+ "panel.contentError": "콘텐츠 검색 실패: {error}",
2384
+ "panel.noContent": "일치하는 콘텐츠가 없습니다.",
2385
+ "panel.contentHint": "키워드를 입력하여 콘텐츠를 검색하세요.",
2386
+ "panel.noText": "(텍스트 없음)",
2387
+ "panel.untitled": "(제목 없음)",
2388
+ "panel.openSession": "세션 열기",
2389
+ "panel.footer.invoke": "열기",
2390
+ "panel.footer.close": "닫기",
2391
+ "filter.all": "전체",
2392
+ "filter.user": "사용자",
2393
+ "filter.reply": "답변",
2394
+ "filter.tool": "도구",
2395
+ "sort.label": "결과 정렬",
2396
+ "sort.relevance": "관련도",
2397
+ "sort.time": "시간",
2398
+ "sort.relevance.hint": "일치 강도순 정렬(기본값).",
2399
+ "sort.time.hint": "세션 마지막 활동 시간 내림차순, 동률 시 일치 강도순.",
2400
+ "type.user/message": "사용자",
2401
+ "type.assistant/message": "답변",
2402
+ "type.tool/call": "도구 호출",
2403
+ "type.tool/result": "도구 결과"
2404
+ },
2405
+ fr: {
2406
+ "card.title": "Index de recherche",
2407
+ "card.description": "Recherche de sessions dans la barre latérale avec bascule titre/contenu. La recherche de contenu utilise l'index indépendant du plugin et ne dépend jamais de l'index full-text officiel DSH.",
2408
+ "card.unavailable": "Espace de noms des paramètres indisponible : vérifiez que le plugin est assemblé dans le profil.",
2409
+ "card.readonly": "Lecture seule",
2410
+ "card.enabled": "Activer la recherche de sessions",
2411
+ "card.enabled.desc": "Afficher l'entrée « Recherche » en bas de la barre latérale.",
2412
+ "card.defaultMode": "Mode de recherche par défaut",
2413
+ "card.defaultMode.desc": "Mode d'ouverture du panneau.",
2414
+ "card.mode.title": "Titre",
2415
+ "card.mode.content": "Contenu",
2416
+ "card.autoSync": "Synchronisation automatique de l'index",
2417
+ "card.autoSync.desc": "Synchronisation incrémentale des journaux de session vers l'index indépendant en arrière-plan.",
2418
+ "card.syncInterval": "Intervalle de synchronisation (secondes)",
2419
+ "card.syncInterval.desc": "Intervalle minimum entre deux synchronisations incrémentales.",
2420
+ "card.archiveKeep": "Archives à conserver",
2421
+ "card.archiveKeep.desc": "Nombre de fichiers d'index archivés conservés à chaque reconstruction.",
2422
+ "card.index": "Index de recherche de contenu",
2423
+ "card.index.desc": "Index prêt : {indexed} sessions collectées.",
2424
+ "card.index.archives": ", {archives} archive(s).",
2425
+ "card.index.empty": "Index indépendant pas encore construit. Cliquez sur « Reconstruire l'index » pour le créer à partir des journaux de session.",
2426
+ "card.index.reading": "Lecture de l'état de l'index…",
2427
+ "card.index.rebuilding": "Reconstruction de l'index… {done}/{total} (l'ancien index reste actif pendant la reconstruction)",
2428
+ "card.index.syncing": "Synchronisation de l'index…",
2429
+ "card.index.failures": "{count} session(s) en échec de synchronisation (voir les journaux Host).",
2430
+ "card.index.rebuildError": "Échec de la reconstruction : {error}",
2431
+ "card.index.rebuild": "Reconstruire l'index",
2432
+ "card.index.rebuilding.btn": "Reconstruction…",
2433
+ "card.index.rebuild.hint": "Non destructif : l'ancien index reste actif pendant la construction ; l'échange est atomique et l'ancien index est archivé.",
2434
+ "card.index.archivedOwner": "La consultation et la suppression des sessions archivées relèvent de dsh-manage-session. Ce plugin lit uniquement l'ensemble archivé et exclut ces sessions de l'index.",
2435
+ "card.index.archivedMissing": "La consultation et la suppression des sessions archivées nécessitent dsh-manage-session, qui n'est pas installé. Ce plugin lit uniquement l'ensemble archivé et exclut ces sessions de l'index.",
2436
+ "card.index.export": "Exporter l'instantané",
2437
+ "card.index.import": "Importer l'instantané",
2438
+ "card.index.exported": "Instantané téléchargé.",
2439
+ "card.index.import.started": "Import démarré : l'index se reconstruit en arrière-plan et bascule une fois terminé.",
2440
+ "card.index.importParse": "Échec de l'analyse de l'instantané ou aucune session valide.",
2441
+ "card.action.failed": "Échec de l'opération : {error}",
2442
+ "panel.titleSearch": "Titre",
2443
+ "panel.contentSearch": "Contenu",
2444
+ "panel.searchTitle": "Rechercher dans les titres…",
2445
+ "panel.searchContent": "Rechercher dans le contenu…",
2446
+ "panel.entry": "Recherche",
2447
+ "panel.buildIndex": "Construire l'index",
2448
+ "panel.archived": "{count} session(s) archivée(s) exclue(s)",
2449
+ "panel.rebuilding": "Reconstruction de l'index… {done}/{total} (l'ancien index reste actif)",
2450
+ "panel.unavailable": "Service d'index indépendant indisponible : Host non initialisé.",
2451
+ "panel.notBuilt": "Index indépendant pas encore construit : construisez-le pour activer la recherche de contenu (aucun index FTS officiel requis).",
2452
+ "panel.loadingSessions": "Chargement des sessions…",
2453
+ "panel.sessionsError": "Échec du chargement des sessions : {error}",
2454
+ "panel.noSessions": "Aucune session",
2455
+ "panel.noMatch": "Aucune session correspondante.",
2456
+ "panel.loadingContent": "Recherche dans le contenu…",
2457
+ "panel.contentError": "Échec de la recherche de contenu : {error}",
2458
+ "panel.noContent": "Aucun contenu correspondant.",
2459
+ "panel.contentHint": "Saisissez des mots-clés pour rechercher dans le contenu.",
2460
+ "panel.noText": "(pas de texte)",
2461
+ "panel.untitled": "(sans titre)",
2462
+ "panel.openSession": "Ouvrir la session",
2463
+ "panel.footer.invoke": "Ouvrir",
2464
+ "panel.footer.close": "Fermer",
2465
+ "filter.all": "Tout",
2466
+ "filter.user": "Utilisateur",
2467
+ "filter.reply": "Réponse",
2468
+ "filter.tool": "Outil",
2469
+ "sort.label": "Ordre des résultats",
2470
+ "sort.relevance": "Pertinence",
2471
+ "sort.time": "Temps",
2472
+ "sort.relevance.hint": "Trier par force de correspondance (par défaut).",
2473
+ "sort.time.hint": "Trier par dernière activité, plus récent d'abord ; à égalité, par force de correspondance.",
2474
+ "type.user/message": "Utilisateur",
2475
+ "type.assistant/message": "Réponse",
2476
+ "type.tool/call": "Appel d'outil",
2477
+ "type.tool/result": "Résultat d'outil"
2478
+ },
2479
+ de: {
2480
+ "card.title": "Suchindex",
2481
+ "card.description": "Sidebar-Sitzungssuche mit Titel-/Inhaltsmodus-Umschaltung. Die Inhaltssuche verwendet den plugin-eigenen Index und hängt nie vom offiziellen DSH-Volltextindex ab.",
2482
+ "card.unavailable": "Einstellungs-Namensraum nicht verfügbar: Stellen Sie sicher, dass das Plugin im Profil zusammengestellt ist.",
2483
+ "card.readonly": "Schreibgeschützt",
2484
+ "card.enabled": "Sitzungssuche aktivieren",
2485
+ "card.enabled.desc": "Den „Suche\"-Eintrag am unteren Rand der Sidebar anzeigen.",
2486
+ "card.defaultMode": "Standardsuchmodus",
2487
+ "card.defaultMode.desc": "In welchem Modus das Panel geöffnet wird.",
2488
+ "card.mode.title": "Titel",
2489
+ "card.mode.content": "Inhalt",
2490
+ "card.autoSync": "Index automatisch synchronisieren",
2491
+ "card.autoSync.desc": "Sitzungsprotokolle inkrementell in den unabhängigen Index im Hintergrund synchronisieren.",
2492
+ "card.syncInterval": "Synchronisierungsintervall (Sekunden)",
2493
+ "card.syncInterval.desc": "Mindestintervall zwischen zwei inkrementellen Synchronisierungen.",
2494
+ "card.archiveKeep": "Aufzubewahrende Archive",
2495
+ "card.archiveKeep.desc": "Anzahl der archivierten Indexdateien, die jeder Neuaufbau beibehält.",
2496
+ "card.index": "Inhaltssuchindex",
2497
+ "card.index.desc": "Index bereit: {indexed} Sitzungen erfasst.",
2498
+ "card.index.archives": ", {archives} Archiv(e).",
2499
+ "card.index.empty": "Unabhängiger Index noch nicht aufgebaut. Klicken Sie auf „Index neu aufbauen\", um ihn aus Sitzungsprotokollen zu erstellen.",
2500
+ "card.index.reading": "Indexstatus wird gelesen…",
2501
+ "card.index.rebuilding": "Index wird neu aufgebaut… {done}/{total} (der alte Index bleibt während des Neuaufbaus aktiv)",
2502
+ "card.index.syncing": "Index wird synchronisiert…",
2503
+ "card.index.failures": "{count} Sitzung(en) konnten nicht synchronisiert werden (siehe Host-Protokolle).",
2504
+ "card.index.rebuildError": "Neuaufbau fehlgeschlagen: {error}",
2505
+ "card.index.rebuild": "Index neu aufbauen",
2506
+ "card.index.rebuilding.btn": "Wird aufgebaut…",
2507
+ "card.index.rebuild.hint": "Zerstörungsfrei: Der alte Index bleibt aktiv, während der Schatten-Index aufgebaut wird; der Tausch erfolgt atomar und der alte Index wird archiviert.",
2508
+ "card.index.archivedOwner": "Das Durchsuchen und Entsorgen archivierter Sitzungen gehört zu dsh-manage-session. Dieses Plugin liest nur das Archiv-Set und schließt diese Sitzungen aus dem Index aus.",
2509
+ "card.index.archivedMissing": "Das Durchsuchen und Entsorgen archivierter Sitzungen erfordert dsh-manage-session, das nicht installiert ist. Dieses Plugin liest nur das Archiv-Set und schließt diese Sitzungen aus dem Index aus.",
2510
+ "card.index.export": "Snapshot exportieren",
2511
+ "card.index.import": "Snapshot importieren",
2512
+ "card.index.exported": "Snapshot heruntergeladen.",
2513
+ "card.index.import.started": "Import gestartet: Der Index wird im Hintergrund neu aufgebaut und nach Fertigstellung atomar ausgetauscht.",
2514
+ "card.index.importParse": "Snapshot-Analyse fehlgeschlagen oder enthält keine gültige Sitzung.",
2515
+ "card.action.failed": "Aktion fehlgeschlagen: {error}",
2516
+ "panel.titleSearch": "Titel",
2517
+ "panel.contentSearch": "Inhalt",
2518
+ "panel.searchTitle": "Sitzungstitel durchsuchen…",
2519
+ "panel.searchContent": "Sitzungsinhalt durchsuchen…",
2520
+ "panel.entry": "Suche",
2521
+ "panel.buildIndex": "Index aufbauen",
2522
+ "panel.archived": "{count} archivierte Sitzung(en) ausgeschlossen",
2523
+ "panel.rebuilding": "Index wird neu aufgebaut… {done}/{total} (der alte Index bleibt aktiv)",
2524
+ "panel.unavailable": "Unabhängiger Indexdienst nicht verfügbar: Host nicht initialisiert.",
2525
+ "panel.notBuilt": "Unabhängiger Index noch nicht aufgebaut: Bauen Sie ihn auf, um die Inhaltssuche zu aktivieren (kein offizieller FTS-Index erforderlich).",
2526
+ "panel.loadingSessions": "Sitzungen werden geladen…",
2527
+ "panel.sessionsError": "Sitzungen konnten nicht geladen werden: {error}",
2528
+ "panel.noSessions": "Keine Sitzungen",
2529
+ "panel.noMatch": "Keine passenden Sitzungen.",
2530
+ "panel.loadingContent": "Inhalt wird durchsucht…",
2531
+ "panel.contentError": "Inhaltssuche fehlgeschlagen: {error}",
2532
+ "panel.noContent": "Kein passender Inhalt.",
2533
+ "panel.contentHint": "Geben Sie Stichwörter ein, um Inhalte zu durchsuchen.",
2534
+ "panel.noText": "(kein Text)",
2535
+ "panel.untitled": "(unbenannt)",
2536
+ "panel.openSession": "Sitzung öffnen",
2537
+ "panel.footer.invoke": "Öffnen",
2538
+ "panel.footer.close": "Schließen",
2539
+ "filter.all": "Alle",
2540
+ "filter.user": "Benutzer",
2541
+ "filter.reply": "Antwort",
2542
+ "filter.tool": "Werkzeug",
2543
+ "sort.label": "Ergebnissortierung",
2544
+ "sort.relevance": "Relevanz",
2545
+ "sort.time": "Zeit",
2546
+ "sort.relevance.hint": "Nach Übereinstimmungsstärke sortieren (Standard).",
2547
+ "sort.time.hint": "Nach letzter Sitzungsaktivität, neueste zuerst; bei Gleichstand nach Übereinstimmungsstärke.",
2548
+ "type.user/message": "Benutzer",
2549
+ "type.assistant/message": "Antwort",
2550
+ "type.tool/call": "Werkzeugaufruf",
2551
+ "type.tool/result": "Werkzeugergebnis"
2552
+ },
2553
+ it: {
2554
+ "card.title": "Indice di ricerca",
2555
+ "card.description": "Ricerca sessioni nella barra laterale con commutazione titolo/contenuto. La ricerca per contenuto utilizza l'indice indipendente del plugin e non dipende mai dall'indice full-text ufficiale DSH.",
2556
+ "card.unavailable": "Namespace delle impostazioni non disponibile: verificare che il plugin sia assemblato nel profilo.",
2557
+ "card.readonly": "Sola lettura",
2558
+ "card.enabled": "Abilita ricerca sessioni",
2559
+ "card.enabled.desc": "Mostra la voce «Ricerca» in fondo alla barra laterale.",
2560
+ "card.defaultMode": "Modalità di ricerca predefinita",
2561
+ "card.defaultMode.desc": "Modalità di apertura del pannello.",
2562
+ "card.mode.title": "Titolo",
2563
+ "card.mode.content": "Contenuto",
2564
+ "card.autoSync": "Sincronizzazione automatica indice",
2565
+ "card.autoSync.desc": "Sincronizza incrementalmente i log delle sessioni nell'indice indipendente in background.",
2566
+ "card.syncInterval": "Intervallo sincronizzazione (secondi)",
2567
+ "card.syncInterval.desc": "Intervallo minimo tra due sincronizzazioni incrementali.",
2568
+ "card.archiveKeep": "Archivi da conservare",
2569
+ "card.archiveKeep.desc": "Numero di file indice archiviati conservati ad ogni ricostruzione.",
2570
+ "card.index": "Indice ricerca contenuti",
2571
+ "card.index.desc": "Indice pronto: {indexed} sessioni raccolte.",
2572
+ "card.index.archives": ", {archives} archivio/i.",
2573
+ "card.index.empty": "Indice indipendente non ancora costruito. Fare clic su «Ricostruisci indice» per crearlo dai log delle sessioni.",
2574
+ "card.index.reading": "Lettura stato indice…",
2575
+ "card.index.rebuilding": "Ricostruzione indice… {done}/{total} (il vecchio indice resta attivo durante la ricostruzione)",
2576
+ "card.index.syncing": "Sincronizzazione indice…",
2577
+ "card.index.failures": "{count} sessione/i non sincronizzata/e (vedi log Host).",
2578
+ "card.index.rebuildError": "Ricostruzione fallita: {error}",
2579
+ "card.index.rebuild": "Ricostruisci indice",
2580
+ "card.index.rebuilding.btn": "Ricostruzione…",
2581
+ "card.index.rebuild.hint": "Non distruttivo: il vecchio indice resta attivo durante la costruzione; lo scambio è atomico e il vecchio indice viene archiviato.",
2582
+ "card.index.archivedOwner": "La consultazione e l'eliminazione delle sessioni archiviate spettano a dsh-manage-session. Questo plugin legge solo l'insieme archiviato ed esclude tali sessioni dall'indice.",
2583
+ "card.index.archivedMissing": "La consultazione e l'eliminazione delle sessioni archiviate richiedono dsh-manage-session, attualmente non installato. Questo plugin legge solo l'insieme archiviato ed esclude tali sessioni dall'indice.",
2584
+ "card.index.export": "Esporta snapshot",
2585
+ "card.index.import": "Importa snapshot",
2586
+ "card.index.exported": "Snapshot scaricato.",
2587
+ "card.index.import.started": "Importazione avviata: l'indice viene ricostruito in background e scambiato al termine.",
2588
+ "card.index.importParse": "Analisi snapshot fallita o nessuna sessione valida.",
2589
+ "card.action.failed": "Operazione fallita: {error}",
2590
+ "panel.titleSearch": "Titolo",
2591
+ "panel.contentSearch": "Contenuto",
2592
+ "panel.searchTitle": "Cerca nei titoli delle sessioni…",
2593
+ "panel.searchContent": "Cerca nel contenuto delle sessioni…",
2594
+ "panel.entry": "Ricerca",
2595
+ "panel.buildIndex": "Costruisci indice",
2596
+ "panel.archived": "{count} sessione/i archiviata/e esclusa/e",
2597
+ "panel.rebuilding": "Ricostruzione indice… {done}/{total} (il vecchio indice resta attivo)",
2598
+ "panel.unavailable": "Servizio indice indipendente non disponibile: Host non inizializzato.",
2599
+ "panel.notBuilt": "Indice indipendente non ancora costruito: costruiscilo per abilitare la ricerca per contenuto (nessun indice FTS ufficiale richiesto).",
2600
+ "panel.loadingSessions": "Caricamento sessioni…",
2601
+ "panel.sessionsError": "Caricamento sessioni fallito: {error}",
2602
+ "panel.noSessions": "Nessuna sessione",
2603
+ "panel.noMatch": "Nessuna sessione corrispondente.",
2604
+ "panel.loadingContent": "Ricerca nel contenuto…",
2605
+ "panel.contentError": "Ricerca contenuto fallita: {error}",
2606
+ "panel.noContent": "Nessun contenuto corrispondente.",
2607
+ "panel.contentHint": "Inserisci parole chiave per cercare nel contenuto.",
2608
+ "panel.noText": "(nessun testo)",
2609
+ "panel.untitled": "(senza titolo)",
2610
+ "panel.openSession": "Apri sessione",
2611
+ "panel.footer.invoke": "Apri",
2612
+ "panel.footer.close": "Chiudi",
2613
+ "filter.all": "Tutti",
2614
+ "filter.user": "Utente",
2615
+ "filter.reply": "Risposta",
2616
+ "filter.tool": "Strumento",
2617
+ "sort.label": "Ordinamento risultati",
2618
+ "sort.relevance": "Pertinenza",
2619
+ "sort.time": "Tempo",
2620
+ "sort.relevance.hint": "Ordina per forza di corrispondenza (predefinito).",
2621
+ "sort.time.hint": "Ordina per ultima attività, più recente prima; a parità, per forza di corrispondenza.",
2622
+ "type.user/message": "Utente",
2623
+ "type.assistant/message": "Risposta",
2624
+ "type.tool/call": "Chiamata strumento",
2625
+ "type.tool/result": "Risultato strumento"
2626
+ },
2627
+ ru: {
2628
+ "card.title": "Поисковый индекс",
2629
+ "card.description": "Поиск сессий в боковой панели с переключением «заголовки/содержимое». Поиск по содержимому использует собственный индекс плагина и не зависит от официального полнотекстового индекса DSH.",
2630
+ "card.unavailable": "Пространство имён настроек недоступно: убедитесь, что плагин собран в профиле.",
2631
+ "card.readonly": "Только чтение",
2632
+ "card.enabled": "Включить поиск сессий",
2633
+ "card.enabled.desc": "Показывать пункт «Поиск» в нижней части боковой панели.",
2634
+ "card.defaultMode": "Режим поиска по умолчанию",
2635
+ "card.defaultMode.desc": "Режим открытия панели.",
2636
+ "card.mode.title": "Заголовок",
2637
+ "card.mode.content": "Содержимое",
2638
+ "card.autoSync": "Автосинхронизация индекса",
2639
+ "card.autoSync.desc": "Инкрементальная синхронизация журналов сессий в независимый индекс в фоновом режиме.",
2640
+ "card.syncInterval": "Интервал синхронизации (сек)",
2641
+ "card.syncInterval.desc": "Минимальный интервал между двумя инкрементальными синхронизациями.",
2642
+ "card.archiveKeep": "Хранимых архивов",
2643
+ "card.archiveKeep.desc": "Сколько архивных файлов индекса сохранять при каждой пересборке.",
2644
+ "card.index": "Индекс поиска по содержимому",
2645
+ "card.index.desc": "Индекс готов: собрано {indexed} сессий.",
2646
+ "card.index.archives": ", архивов: {archives}.",
2647
+ "card.index.empty": "Независимый индекс ещё не построен. Нажмите «Пересобрать индекс», чтобы создать его из журналов сессий.",
2648
+ "card.index.reading": "Чтение состояния индекса…",
2649
+ "card.index.rebuilding": "Пересборка индекса… {done}/{total} (старый индекс остаётся доступным во время пересборки)",
2650
+ "card.index.syncing": "Синхронизация индекса…",
2651
+ "card.index.failures": "{count} сессий не синхронизировано (см. журналы Host).",
2652
+ "card.index.rebuildError": "Пересборка не удалась: {error}",
2653
+ "card.index.rebuild": "Пересобрать индекс",
2654
+ "card.index.rebuilding.btn": "Пересборка…",
2655
+ "card.index.rebuild.hint": "Недеструктивно: старый индекс остаётся доступным, пока строится теневой; замена атомарна, старый индекс архивируется.",
2656
+ "card.index.archivedOwner": "Просмотр и удаление архивных сессий — задача dsh-manage-session. Этот плагин только читает набор архива и исключает архивные сессии из индекса.",
2657
+ "card.index.archivedMissing": "Для просмотра и удаления архивных сессий требуется dsh-manage-session, который не установлен. Этот плагин только читает набор архива и исключает архивные сессии из индекса.",
2658
+ "card.index.export": "Экспорт снимка",
2659
+ "card.index.import": "Импорт снимка",
2660
+ "card.index.exported": "Снимок скачан.",
2661
+ "card.index.import.started": "Импорт начат: индекс пересобирается в фоне и переключается по завершении.",
2662
+ "card.index.importParse": "Ошибка разбора снимка или нет допустимых сессий.",
2663
+ "card.action.failed": "Действие не выполнено: {error}",
2664
+ "panel.titleSearch": "Заголовок",
2665
+ "panel.contentSearch": "Содержимое",
2666
+ "panel.searchTitle": "Поиск по заголовкам сессий…",
2667
+ "panel.searchContent": "Поиск по содержимому сессий…",
2668
+ "panel.entry": "Поиск",
2669
+ "panel.buildIndex": "Построить индекс",
2670
+ "panel.archived": "Исключено {count} архивных сессий",
2671
+ "panel.rebuilding": "Пересборка индекса… {done}/{total} (старый индекс остаётся доступным)",
2672
+ "panel.unavailable": "Сервис независимого индекса недоступен: Host не инициализирован.",
2673
+ "panel.notBuilt": "Независимый индекс не построен: постройте его, чтобы включить поиск по содержимому (официальный FTS-индекс не требуется).",
2674
+ "panel.loadingSessions": "Загрузка сессий…",
2675
+ "panel.sessionsError": "Не удалось загрузить сессии: {error}",
2676
+ "panel.noSessions": "Нет сессий",
2677
+ "panel.noMatch": "Нет подходящих сессий.",
2678
+ "panel.loadingContent": "Поиск по содержимому…",
2679
+ "panel.contentError": "Поиск по содержимому не удался: {error}",
2680
+ "panel.noContent": "Нет подходящего содержимого.",
2681
+ "panel.contentHint": "Введите ключевые слова для поиска по содержимому.",
2682
+ "panel.noText": "(без текста)",
2683
+ "panel.untitled": "(без названия)",
2684
+ "panel.openSession": "Открыть сессию",
2685
+ "panel.footer.invoke": "Открыть",
2686
+ "panel.footer.close": "Закрыть",
2687
+ "filter.all": "Все",
2688
+ "filter.user": "Пользователь",
2689
+ "filter.reply": "Ответ",
2690
+ "filter.tool": "Инструмент",
2691
+ "sort.label": "Сортировка результатов",
2692
+ "sort.relevance": "Релевантность",
2693
+ "sort.time": "Время",
2694
+ "sort.relevance.hint": "Сортировать по силе совпадения (по умолчанию).",
2695
+ "sort.time.hint": "Сортировать по последней активности, сначала новые; при равенстве — по силе совпадения.",
2696
+ "type.user/message": "Пользователь",
2697
+ "type.assistant/message": "Ответ",
2698
+ "type.tool/call": "Вызов инструмента",
2699
+ "type.tool/result": "Результат инструмента"
2700
+ },
2701
+ es: {
2702
+ "card.title": "Índice de búsqueda",
2703
+ "card.description": "Búsqueda de sesiones en la barra lateral con cambio entre título y contenido. La búsqueda de contenido usa el índice propio del plugin y nunca depende del índice de texto completo oficial de DSH.",
2704
+ "card.unavailable": "Espacio de nombres de configuración no disponible: verifique que el plugin esté ensamblado en el perfil.",
2705
+ "card.readonly": "Solo lectura",
2706
+ "card.enabled": "Habilitar búsqueda de sesiones",
2707
+ "card.enabled.desc": "Mostrar la entrada «Buscar» en la parte inferior de la barra lateral.",
2708
+ "card.defaultMode": "Modo de búsqueda predeterminado",
2709
+ "card.defaultMode.desc": "Modo en que se abre el panel.",
2710
+ "card.mode.title": "Título",
2711
+ "card.mode.content": "Contenido",
2712
+ "card.autoSync": "Sincronización automática del índice",
2713
+ "card.autoSync.desc": "Sincroniza incrementalmente los registros de sesión al índice independiente en segundo plano.",
2714
+ "card.syncInterval": "Intervalo de sincronización (segundos)",
2715
+ "card.syncInterval.desc": "Intervalo mínimo entre dos sincronizaciones incrementales.",
2716
+ "card.archiveKeep": "Archivos a conservar",
2717
+ "card.archiveKeep.desc": "Cuántos archivos de índice archivados se conservan en cada reconstrucción.",
2718
+ "card.index": "Índice de búsqueda de contenido",
2719
+ "card.index.desc": "Índice listo: {indexed} sesiones recopiladas.",
2720
+ "card.index.archives": ", {archives} archivo(s) de respaldo.",
2721
+ "card.index.empty": "El índice independiente aún no está construido. Haga clic en «Reconstruir índice» para crearlo desde los registros de sesión.",
2722
+ "card.index.reading": "Leyendo estado del índice…",
2723
+ "card.index.rebuilding": "Reconstruyendo índice… {done}/{total} (el índice anterior sigue activo durante la reconstrucción)",
2724
+ "card.index.syncing": "Sincronizando índice…",
2725
+ "card.index.failures": "{count} sesión(es) fallaron al sincronizar (ver registros del Host).",
2726
+ "card.index.rebuildError": "Reconstrucción fallida: {error}",
2727
+ "card.index.rebuild": "Reconstruir índice",
2728
+ "card.index.rebuilding.btn": "Reconstruyendo…",
2729
+ "card.index.rebuild.hint": "No destructivo: el índice anterior sigue activo mientras se construye el nuevo; el intercambio es atómico y el índice anterior se archiva.",
2730
+ "card.index.archivedOwner": "La consulta y eliminación de sesiones archivadas corresponde a dsh-manage-session. Este plugin solo lee el conjunto archivado y excluye esas sesiones del índice.",
2731
+ "card.index.archivedMissing": "La consulta y eliminación de sesiones archivadas requiere dsh-manage-session, que no está instalado. Este plugin solo lee el conjunto archivado y excluye esas sesiones del índice.",
2732
+ "card.index.export": "Exportar instantánea",
2733
+ "card.index.import": "Importar instantánea",
2734
+ "card.index.exported": "Instantánea descargada.",
2735
+ "card.index.import.started": "Importación iniciada: el índice se reconstruye en segundo plano y se intercambia al finalizar.",
2736
+ "card.index.importParse": "Error al analizar la instantánea o sin sesiones válidas.",
2737
+ "card.action.failed": "Acción fallida: {error}",
2738
+ "panel.titleSearch": "Título",
2739
+ "panel.contentSearch": "Contenido",
2740
+ "panel.searchTitle": "Buscar títulos de sesión…",
2741
+ "panel.searchContent": "Buscar contenido de sesión…",
2742
+ "panel.entry": "Buscar",
2743
+ "panel.buildIndex": "Construir índice",
2744
+ "panel.archived": "{count} sesión(es) archivada(s) excluida(s)",
2745
+ "panel.rebuilding": "Reconstruyendo índice… {done}/{total} (el índice anterior sigue activo)",
2746
+ "panel.unavailable": "Servicio de índice independiente no disponible: Host no inicializado.",
2747
+ "panel.notBuilt": "Índice independiente no construido: constrúyalo para habilitar la búsqueda de contenido (no se requiere índice FTS oficial).",
2748
+ "panel.loadingSessions": "Cargando sesiones…",
2749
+ "panel.sessionsError": "Error al cargar sesiones: {error}",
2750
+ "panel.noSessions": "Sin sesiones",
2751
+ "panel.noMatch": "Sin sesiones coincidentes.",
2752
+ "panel.loadingContent": "Buscando en el contenido…",
2753
+ "panel.contentError": "Búsqueda de contenido fallida: {error}",
2754
+ "panel.noContent": "Sin contenido coincidente.",
2755
+ "panel.contentHint": "Escriba palabras clave para buscar contenido.",
2756
+ "panel.noText": "(sin texto)",
2757
+ "panel.untitled": "(sin título)",
2758
+ "panel.openSession": "Abrir sesión",
2759
+ "panel.footer.invoke": "Abrir",
2760
+ "panel.footer.close": "Cerrar",
2761
+ "filter.all": "Todos",
2762
+ "filter.user": "Usuario",
2763
+ "filter.reply": "Respuesta",
2764
+ "filter.tool": "Herramienta",
2765
+ "sort.label": "Orden de resultados",
2766
+ "sort.relevance": "Relevancia",
2767
+ "sort.time": "Tiempo",
2768
+ "sort.relevance.hint": "Ordenar por fuerza de coincidencia (predeterminado).",
2769
+ "sort.time.hint": "Ordenar por última actividad, más reciente primero; en caso de empate, por fuerza de coincidencia.",
2770
+ "type.user/message": "Usuario",
2771
+ "type.assistant/message": "Respuesta",
2772
+ "type.tool/call": "Llamada de herramienta",
2773
+ "type.tool/result": "Resultado de herramienta"
2774
+ }
2775
+ };
2776
+ /** Translate with {param} interpolation; falls back to zh, then the key itself. */
2777
+ function translate$1(locale, key, params) {
2778
+ const raw = locale?.(key, params);
2779
+ if (typeof raw === "string" && raw !== "" && raw !== key) return raw;
2780
+ const template = dictionaries.zh[key] ?? key;
2781
+ if (params === void 0) return template;
2782
+ return template.replace(/\{(\w+)\}/gu, (_, name) => String(params[name] ?? `{${name}}`));
2783
+ }
2784
+ //#endregion
2785
+ //#region src/client/search/manage.tsx
2786
+ /**
2787
+ * 批量管理台(workspace 视角)—— 管家面板「管理」页签的主体。
2788
+ *
2789
+ * beta.5 评审:管理是管家职能,不该出现在搜索面板的模式切换里;本组件从
2790
+ * search/panel.tsx 抽出,挂在 StewardPanel 第三页签。样式自带注入(幂等),
2791
+ * 与搜索面板是否启用解耦。
2792
+ *
2793
+ * 数据面:list-sessions 读统一索引语料(活跃+归档,archived 随行);
2794
+ * 动作面:session-history-archive / prune / purge(写官方存储文件,重启后
2795
+ * 侧栏完全生效——宿主半在成功路径上已同步联动索引)。
2796
+ */
2797
+ /** 样式(幂等注入;类名沿用 dsws_ 前缀,与搜索面板共享视觉语言)。 */
2798
+ function injectManageStyles() {
2799
+ if (typeof document === "undefined") return () => {};
2800
+ if (document.querySelector("style[data-plugin-css=\"dsh-manage-session/manage\"]") !== null) return () => {};
2801
+ const tag = document.createElement("style");
2802
+ tag.dataset.plugin = "dsh-manage-session";
2803
+ tag.dataset.pluginCss = "dsh-manage-session/manage";
2804
+ tag.textContent = `
2805
+ .dsws_manageList{flex:1 1 auto;min-height:0;overflow-y:auto;margin:8px 0 0;padding:0 6px 8px}
2806
+ .dsws_group{margin-bottom:6px}
2807
+ .dsws_groupHead{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:5px 8px;background:var(--dsw-specific-tip);border-bottom:1px solid var(--dsw-alias-border-l2);cursor:pointer;user-select:none}
2808
+ .dsws_groupTitle{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}
2809
+ .dsws_groupCount{flex:none;color:var(--dsw-alias-label-caption);font-size:11px;font-variant-numeric:tabular-nums}
2810
+ .dsws_check{flex:none;display:inline-flex;align-items:center}
2811
+ .dsws_check input{width:13px;height:13px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}
2812
+ .dsws_tagArch{flex:none;color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-caption));font-size:10px;line-height:16px;border:1px solid currentColor;border-radius:999px;padding:0 6px;white-space:nowrap}
2813
+ .dsws_batchBar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-top:1px solid var(--dsw-alias-border-l1)}
2814
+ .dsws_batchInfo{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
2815
+ .dsws_btnDanger{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
2816
+ .dsws_manageHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:8px 10px 0}
2817
+ .dsws_chips{display:flex;align-items:center;gap:6px;padding:8px 10px 0;flex:none;flex-wrap:wrap}
2818
+ .dsws_chip{height:24px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:999px;padding:0 10px;font-size:12px;font-weight:500;line-height:22px;white-space:nowrap}
2819
+ .dsws_chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
2820
+ .dsws_chipActive{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
2821
+ `;
2822
+ document.head.appendChild(tag);
2823
+ return () => {
2824
+ if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
2825
+ };
2826
+ }
2827
+ /** 批量管理台主体(自持语料/勾选/批量状态)。 */
2828
+ function ManageConsole({ t, open }) {
2829
+ const [sessions, setSessions] = (0, react.useState)(null);
2830
+ const [sessionsError, setSessionsError] = (0, react.useState)(null);
2831
+ const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
2832
+ const [batch, setBatch] = (0, react.useState)({
2833
+ busy: false,
2834
+ message: "",
2835
+ ok: true
2836
+ });
2837
+ const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
2838
+ const [domain, setDomain] = (0, react.useState)("all");
2839
+ const [favorites, setFavorites] = (0, react.useState)(/* @__PURE__ */ new Set());
2840
+ const [favoritesOnly, setFavoritesOnly] = (0, react.useState)(false);
2841
+ const [workspaceFilter, setWorkspaceFilter] = (0, react.useState)("");
2842
+ const [textFilter, setTextFilter] = (0, react.useState)("");
2843
+ const [showAllWorkspaces, setShowAllWorkspaces] = (0, react.useState)(false);
2844
+ const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
1190
2845
  (0, react.useEffect)(() => {
1191
- const onKey = (event) => {
1192
- if (event.key === "Escape") onClose();
2846
+ let cancelled = false;
2847
+ callSteward("session-history-favorites-list", {}).then((res) => {
2848
+ if (cancelled || !res.ok || !Array.isArray(res.favorites)) return;
2849
+ setFavorites(new Set(res.favorites));
2850
+ });
2851
+ return () => {
2852
+ cancelled = true;
1193
2853
  };
1194
- document.addEventListener("keydown", onKey);
2854
+ }, []);
2855
+ /** 星标切换:写 host 收藏域,就地更新本地集合。 */
2856
+ const toggleFavorite = (sessionId) => {
2857
+ const next = !favorites.has(sessionId);
2858
+ setFavorites((prev) => {
2859
+ const copy = new Set(prev);
2860
+ if (next) copy.add(sessionId);
2861
+ else copy.delete(sessionId);
2862
+ return copy;
2863
+ });
2864
+ callSteward("session-history-favorite-set", {
2865
+ sessionId,
2866
+ favorite: next
2867
+ });
2868
+ };
2869
+ (0, react.useEffect)(() => {
2870
+ if (sessions !== null) return;
2871
+ let cancelled = false;
2872
+ callHost("list-sessions", {}).then((res) => {
2873
+ if (cancelled) return;
2874
+ if (res.ok) {
2875
+ setSessions(res.items);
2876
+ setSelected(/* @__PURE__ */ new Set());
2877
+ setConfirmPurge(false);
2878
+ } else setSessionsError(res.error ?? "读取会话列表失败");
2879
+ });
1195
2880
  return () => {
1196
- document.removeEventListener("keydown", onKey);
2881
+ cancelled = true;
1197
2882
  };
1198
- }, [onClose]);
1199
- const tabs = [];
1200
- if (historyFiles) tabs.push((0, react.createElement)("button", {
1201
- key: TAB_HISTORY,
1202
- id: TAB_HISTORY,
1203
- type: "button",
1204
- className: `dss_tab${tab === "history" ? " dss_tabActive" : ""}`,
1205
- "aria-selected": tab === "history",
1206
- onClick: () => {
1207
- setTab("history");
2883
+ }, [sessions]);
2884
+ const manageGroups = (0, react.useMemo)(() => {
2885
+ if (sessions === null) return [];
2886
+ const byCwd = /* @__PURE__ */ new Map();
2887
+ for (const item of sessions) {
2888
+ if (domain === "active" && item.archived === true) continue;
2889
+ if (domain === "archived" && item.archived !== true) continue;
2890
+ if (favoritesOnly && !favorites.has(item.sessionId)) continue;
2891
+ if (workspaceFilter !== "" && item.cwd !== workspaceFilter) continue;
2892
+ const list = byCwd.get(item.cwd);
2893
+ if (list === void 0) byCwd.set(item.cwd, [item]);
2894
+ else list.push(item);
1208
2895
  }
1209
- }, translate(t, "panel.tab.history")));
1210
- if (healthCheck) tabs.push((0, react.createElement)("button", {
1211
- key: TAB_HEALTH,
1212
- id: TAB_HEALTH,
1213
- type: "button",
1214
- className: `dss_tab${tab === "health" ? " dss_tabActive" : ""}`,
1215
- "aria-selected": tab === "health",
1216
- onClick: () => {
1217
- setTab("health");
2896
+ return [...byCwd.entries()].sort((a, b) => (a[0] === "" ? 1 : 0) - (b[0] === "" ? 1 : 0) || a[0].localeCompare(b[0])).map(([cwd, items]) => ({
2897
+ cwd,
2898
+ items
2899
+ }));
2900
+ }, [
2901
+ sessions,
2902
+ domain,
2903
+ favoritesOnly,
2904
+ favorites,
2905
+ workspaceFilter
2906
+ ]);
2907
+ /** workspace chip 数据:全部/各 workspace 计数,按计数降序,未分组最后。 */
2908
+ const workspaceChips = (0, react.useMemo)(() => {
2909
+ if (sessions === null) return [];
2910
+ const counts = /* @__PURE__ */ new Map();
2911
+ for (const item of sessions) {
2912
+ if (domain === "active" && item.archived === true) continue;
2913
+ if (domain === "archived" && item.archived !== true) continue;
2914
+ if (favoritesOnly && !favorites.has(item.sessionId)) continue;
2915
+ counts.set(item.cwd, (counts.get(item.cwd) ?? 0) + 1);
1218
2916
  }
1219
- }, translate(t, "panel.tab.health")));
1220
- const body = tab === "history" && historyFiles ? (0, react.createElement)(HistoryPanel, {
1221
- t,
1222
- onClose
1223
- }) : tab === "health" && healthCheck ? (0, react.createElement)(HealthPanel, {
1224
- t,
1225
- onClose
1226
- }) : (0, react.createElement)("div", { className: "dss_empty" }, translate(t, "card.enabled.desc"));
1227
- return (0, react_dom.createPortal)((0, react.createElement)("div", { key: "steward-root" }, [(0, react.createElement)("div", {
1228
- key: "backdrop",
1229
- className: "dss_backdrop",
1230
- onClick: onClose
1231
- }), (0, react.createElement)("div", {
1232
- key: "panel",
1233
- className: "dss_panel",
1234
- role: "dialog",
1235
- "aria-label": translate(t, "panel.title")
1236
- }, [(0, react.createElement)("div", {
1237
- key: "head",
1238
- className: "dss_dialogHead"
2917
+ return [...counts.entries()].sort((a, b) => (a[0] === "" ? 1 : 0) - (b[0] === "" ? 1 : 0) || b[1] - a[1]).map(([cwd, count]) => ({
2918
+ cwd,
2919
+ count,
2920
+ label: cwd === "" ? translate$1(t, "manage.group.nocwd") : cwd.split(/[\/]/).pop() ?? cwd
2921
+ }));
1239
2922
  }, [
1240
- (0, react.createElement)("span", {
1241
- key: "title",
1242
- className: "dss_dialogTitle"
1243
- }, translate(t, "panel.title")),
1244
- (0, react.createElement)("span", {
1245
- key: "tabs",
1246
- className: "dss_tabRow"
1247
- }, tabs),
1248
- (0, react.createElement)("button", {
1249
- key: "close",
2923
+ sessions,
2924
+ domain,
2925
+ favoritesOnly,
2926
+ favorites,
2927
+ t
2928
+ ]);
2929
+ /** chip 折叠:默认最多 8 个,超出折叠进「还有 N 个」。 */
2930
+ const visibleChips = (0, react.useMemo)(() => {
2931
+ if (showAllWorkspaces) return {
2932
+ chips: workspaceChips,
2933
+ hidden: 0
2934
+ };
2935
+ return {
2936
+ chips: workspaceChips.slice(0, 8),
2937
+ hidden: Math.max(0, workspaceChips.length - 8)
2938
+ };
2939
+ }, [workspaceChips, showAllWorkspaces]);
2940
+ /** 文本过滤:标题/cwd 子串,大小写折叠。 */
2941
+ const matchesText = (item) => {
2942
+ const needle = textFilter.trim().toLowerCase();
2943
+ if (needle === "") return true;
2944
+ return item.title.toLowerCase().includes(needle) || item.cwd.toLowerCase().includes(needle);
2945
+ };
2946
+ const toggleCollapsed = (key) => {
2947
+ setCollapsedGroups((prev) => {
2948
+ const next = new Set(prev);
2949
+ if (next.has(key)) next.delete(key);
2950
+ else next.add(key);
2951
+ return next;
2952
+ });
2953
+ };
2954
+ const setAllCollapsed = (collapsed) => {
2955
+ setCollapsedGroups(collapsed ? new Set(manageGroups.map((group) => group.cwd)) : /* @__PURE__ */ new Set());
2956
+ };
2957
+ (0, react.useEffect)(() => {
2958
+ if (workspaceFilter === "") return;
2959
+ setCollapsedGroups((prev) => {
2960
+ if (!prev.has(workspaceFilter)) return prev;
2961
+ const next = new Set(prev);
2962
+ next.delete(workspaceFilter);
2963
+ return next;
2964
+ });
2965
+ }, [workspaceFilter]);
2966
+ /** 批量动作;删除两步确认。 */
2967
+ const runBatch = (kind) => {
2968
+ const ids = [...selected];
2969
+ if (ids.length === 0 || batch.busy) return;
2970
+ if (kind === "purge" && !confirmPurge) {
2971
+ setConfirmPurge(true);
2972
+ return;
2973
+ }
2974
+ setConfirmPurge(false);
2975
+ setBatch({
2976
+ busy: true,
2977
+ message: "",
2978
+ ok: true
2979
+ });
2980
+ callSteward(kind === "archive" ? "session-history-archive" : kind === "unarchive" ? "session-history-prune" : "session-history-purge", { sessionIds: ids }).then((res) => {
2981
+ const base = !res.ok ? translate$1(t, "manage.done.error", { error: res.error ?? "?" }) : kind === "archive" ? translate$1(t, "manage.done.archive", { n: res.added ?? ids.length }) : kind === "unarchive" ? translate$1(t, "manage.done.unarchive", { n: res.removed ?? ids.length }) : translate$1(t, "manage.done.purge", { n: res.purged ?? ids.length });
2982
+ const message = res.ok === true && res.requiresRestart === true ? `${base} ${translate$1(t, "manage.done.restart")}` : base;
2983
+ setBatch({
2984
+ busy: false,
2985
+ message,
2986
+ ok: res.ok === true
2987
+ });
2988
+ if (res.ok) {
2989
+ setSelected(/* @__PURE__ */ new Set());
2990
+ setSessions(null);
2991
+ }
2992
+ });
2993
+ };
2994
+ const toggleOne = (sessionId) => {
2995
+ setSelected((prev) => {
2996
+ const next = new Set(prev);
2997
+ if (next.has(sessionId)) next.delete(sessionId);
2998
+ else next.add(sessionId);
2999
+ return next;
3000
+ });
3001
+ };
3002
+ const toggleGroup = (items) => {
3003
+ setSelected((prev) => {
3004
+ const next = new Set(prev);
3005
+ const allIn = items.every((item) => next.has(item.sessionId));
3006
+ for (const item of items) if (allIn) next.delete(item.sessionId);
3007
+ else next.add(item.sessionId);
3008
+ return next;
3009
+ });
3010
+ };
3011
+ (0, react.useEffect)(() => {
3012
+ injectManageStyles();
3013
+ }, []);
3014
+ if (sessionsError !== null) return (0, react.createElement)("div", { className: "dsws_error" }, sessionsError);
3015
+ if (sessions === null) return (0, react.createElement)("div", { className: "dsws_status" }, translate$1(t, "panel.loadingSessions"));
3016
+ if (sessions !== null && manageGroups.length === 0 && (workspaceFilter !== "" || textFilter.trim() !== "" || domain !== "all" || favoritesOnly)) return (0, react.createElement)("div", { className: "dsws_empty" }, translate$1(t, "panel.noMatch"));
3017
+ if (manageGroups.length === 0) return (0, react.createElement)("div", { className: "dsws_empty" }, translate$1(t, "panel.noSessions"));
3018
+ const children = [
3019
+ (0, react.createElement)("input", {
3020
+ key: "search",
3021
+ className: "dsws_search",
3022
+ style: {
3023
+ margin: "8px 10px 0",
3024
+ flex: "none"
3025
+ },
3026
+ type: "text",
3027
+ placeholder: translate$1(t, "manage.search"),
3028
+ value: textFilter,
3029
+ onChange: (e) => {
3030
+ setTextFilter(e.target.value);
3031
+ }
3032
+ }),
3033
+ (0, react.createElement)("div", {
3034
+ key: "wsChips",
3035
+ className: "dsws_chips",
3036
+ role: "group",
3037
+ "aria-label": translate$1(t, "workspace.label")
3038
+ }, [
3039
+ (0, react.createElement)("span", {
3040
+ key: "label",
3041
+ style: {
3042
+ flex: "none",
3043
+ fontSize: "12px",
3044
+ color: "var(--dsw-alias-label-caption)"
3045
+ }
3046
+ }, translate$1(t, "workspace.label")),
3047
+ (0, react.createElement)("button", {
3048
+ key: "all",
3049
+ type: "button",
3050
+ className: `dsws_chip${workspaceFilter === "" ? " dsws_chipActive" : ""}`,
3051
+ "aria-pressed": workspaceFilter === "",
3052
+ onClick: () => {
3053
+ setWorkspaceFilter("");
3054
+ }
3055
+ }, `${translate$1(t, "domain.all")} (${sessions?.length ?? 0})`),
3056
+ ...visibleChips.chips.map((chip) => (0, react.createElement)("button", {
3057
+ key: chip.cwd === "" ? "(nocwd)" : chip.cwd,
3058
+ type: "button",
3059
+ className: `dsws_chip${workspaceFilter === chip.cwd ? " dsws_chipActive" : ""}`,
3060
+ "aria-pressed": workspaceFilter === chip.cwd,
3061
+ onClick: () => {
3062
+ setWorkspaceFilter((current) => current === chip.cwd ? "" : chip.cwd);
3063
+ }
3064
+ }, `${chip.label} (${chip.count})`)),
3065
+ visibleChips.hidden > 0 && (0, react.createElement)("button", {
3066
+ key: "more",
3067
+ type: "button",
3068
+ className: "dsws_chip",
3069
+ onClick: () => {
3070
+ setShowAllWorkspaces(true);
3071
+ }
3072
+ }, translate$1(t, "workspace.more", { n: visibleChips.hidden })),
3073
+ showAllWorkspaces && workspaceChips.length > 8 && (0, react.createElement)("button", {
3074
+ key: "less",
3075
+ type: "button",
3076
+ className: "dsws_chip",
3077
+ onClick: () => {
3078
+ setShowAllWorkspaces(false);
3079
+ }
3080
+ }, translate$1(t, "workspace.less")),
3081
+ (0, react.createElement)("span", {
3082
+ key: "gap",
3083
+ style: { flex: 1 }
3084
+ }),
3085
+ manageGroups.length > 1 && (0, react.createElement)("button", {
3086
+ key: "collapseAll",
3087
+ type: "button",
3088
+ className: "dsws_chip",
3089
+ onClick: () => {
3090
+ setAllCollapsed(true);
3091
+ }
3092
+ }, translate$1(t, "group.collapseAll")),
3093
+ manageGroups.length > 1 && (0, react.createElement)("button", {
3094
+ key: "expandAll",
3095
+ type: "button",
3096
+ className: "dsws_chip",
3097
+ onClick: () => {
3098
+ setAllCollapsed(false);
3099
+ }
3100
+ }, translate$1(t, "group.expandAll"))
3101
+ ]),
3102
+ (0, react.createElement)("div", {
3103
+ key: "chips",
3104
+ className: "dsws_chips",
3105
+ role: "group",
3106
+ "aria-label": translate$1(t, "domain.all")
3107
+ }, [...[
3108
+ "all",
3109
+ "active",
3110
+ "archived"
3111
+ ].map((id) => (0, react.createElement)("button", {
3112
+ key: id,
1250
3113
  type: "button",
1251
- className: "dss_actBtn",
1252
- onClick: onClose
1253
- }, translate(t, "panel.close"))
1254
- ]), body])]), document.body);
3114
+ className: `dsws_chip${domain === id ? " dsws_chipActive" : ""}`,
3115
+ "aria-pressed": domain === id,
3116
+ onClick: () => {
3117
+ setDomain(id);
3118
+ }
3119
+ }, translate$1(t, `domain.${id}`))), (0, react.createElement)("button", {
3120
+ key: "favorites",
3121
+ type: "button",
3122
+ className: `dsws_chip${favoritesOnly ? " dsws_chipActive" : ""}`,
3123
+ "aria-pressed": favoritesOnly,
3124
+ onClick: () => {
3125
+ setFavoritesOnly((v) => !v);
3126
+ }
3127
+ }, `★ ${translate$1(t, "filter.favorites")}`)]),
3128
+ (0, react.createElement)("div", {
3129
+ key: "hint",
3130
+ className: "dsws_manageHint"
3131
+ }, [translate$1(t, "manage.hint"), (0, react.createElement)("span", {
3132
+ key: "sep",
3133
+ style: {
3134
+ display: "block",
3135
+ marginTop: "2px"
3136
+ }
3137
+ }, translate$1(t, "manage.restartHint"))]),
3138
+ (0, react.createElement)("div", {
3139
+ key: "groups",
3140
+ className: "dsws_manageList"
3141
+ }, ...manageGroups.map((group) => (0, react.createElement)("div", {
3142
+ key: group.cwd === "" ? "(nocwd)" : group.cwd,
3143
+ className: "dsws_group"
3144
+ }, [(0, react.createElement)("div", {
3145
+ key: "head",
3146
+ className: "dsws_groupHead",
3147
+ title: translate$1(t, "group.collapse"),
3148
+ onClick: () => {
3149
+ toggleCollapsed(group.cwd);
3150
+ }
3151
+ }, [
3152
+ (0, react.createElement)("span", {
3153
+ key: "chev",
3154
+ style: {
3155
+ flex: "none",
3156
+ fontSize: "10px",
3157
+ color: "var(--dsw-alias-label-caption)",
3158
+ transition: "transform .15s",
3159
+ transform: collapsedGroups.has(group.cwd) ? "rotate(0deg)" : "rotate(90deg)"
3160
+ }
3161
+ }, "▶"),
3162
+ (0, react.createElement)("span", {
3163
+ key: "check",
3164
+ className: "dsws_check"
3165
+ }, (0, react.createElement)("input", {
3166
+ type: "checkbox",
3167
+ checked: group.items.length > 0 && group.items.every((item) => selected.has(item.sessionId)),
3168
+ onChange: () => {
3169
+ toggleGroup(group.items);
3170
+ },
3171
+ onClick: (e) => {
3172
+ e.stopPropagation();
3173
+ },
3174
+ title: translate$1(t, "manage.selectGroup")
3175
+ })),
3176
+ (0, react.createElement)("span", {
3177
+ key: "title",
3178
+ className: "dsws_groupTitle"
3179
+ }, group.cwd === "" ? translate$1(t, "manage.group.nocwd") : group.cwd),
3180
+ (0, react.createElement)("span", {
3181
+ key: "count",
3182
+ className: "dsws_groupCount"
3183
+ }, `${group.items.filter((item) => matchesText(item)).length}/${group.items.length}`)
3184
+ ]), ...collapsedGroups.has(group.cwd) ? [] : group.items.filter((item) => matchesText(item)).map((item) => (0, react.createElement)("button", {
3185
+ key: item.sessionId,
3186
+ type: "button",
3187
+ className: "dsws_row",
3188
+ onClick: () => {
3189
+ open(item.sessionId);
3190
+ }
3191
+ }, [(0, react.createElement)("span", {
3192
+ key: "line",
3193
+ className: "dsws_rowTitle"
3194
+ }, [
3195
+ (0, react.createElement)("span", {
3196
+ key: "check",
3197
+ className: "dsws_check"
3198
+ }, (0, react.createElement)("input", {
3199
+ type: "checkbox",
3200
+ checked: selected.has(item.sessionId),
3201
+ onChange: () => {
3202
+ toggleOne(item.sessionId);
3203
+ },
3204
+ onClick: (e) => {
3205
+ e.stopPropagation();
3206
+ }
3207
+ })),
3208
+ (0, react.createElement)("button", {
3209
+ key: "star",
3210
+ type: "button",
3211
+ className: "dsws_check",
3212
+ title: translate$1(t, favorites.has(item.sessionId) ? "star.on" : "star.off"),
3213
+ style: {
3214
+ border: "none",
3215
+ background: "transparent",
3216
+ cursor: "pointer",
3217
+ fontSize: "13px",
3218
+ lineHeight: 1,
3219
+ padding: "0 2px",
3220
+ color: favorites.has(item.sessionId) ? "var(--dsw-alias-state-warn-label, #d97706)" : "var(--dsw-alias-label-caption)"
3221
+ },
3222
+ onClick: (e) => {
3223
+ e.stopPropagation();
3224
+ toggleFavorite(item.sessionId);
3225
+ }
3226
+ }, favorites.has(item.sessionId) ? "★" : "☆"),
3227
+ (0, react.createElement)("span", {
3228
+ key: "x",
3229
+ className: "dsws_titleText"
3230
+ }, item.title || translate$1(t, "panel.untitled")),
3231
+ item.archived === true && (0, react.createElement)("span", {
3232
+ key: "arch",
3233
+ className: "dsws_tagArch"
3234
+ }, translate$1(t, "tag.archived")),
3235
+ (0, react.createElement)("span", {
3236
+ key: "tag",
3237
+ className: "dsws_tag"
3238
+ }, fmtTime$1(item.updatedAt))
3239
+ ]), (0, react.createElement)("span", {
3240
+ key: "meta",
3241
+ className: "dsws_meta"
3242
+ }, item.cwd)]))])))
3243
+ ];
3244
+ if (selected.size > 0) children.push((0, react.createElement)("div", {
3245
+ key: "batch",
3246
+ className: "dsws_batchBar"
3247
+ }, [
3248
+ (0, react.createElement)("span", {
3249
+ key: "info",
3250
+ className: "dsws_batchInfo"
3251
+ }, batch.message !== "" ? batch.message : translate$1(t, "manage.selected", { n: selected.size })),
3252
+ (0, react.createElement)("button", {
3253
+ key: "archive",
3254
+ type: "button",
3255
+ className: "dsws_actBtn",
3256
+ disabled: batch.busy,
3257
+ onClick: () => {
3258
+ runBatch("archive");
3259
+ }
3260
+ }, translate$1(t, "manage.batch.archive")),
3261
+ (0, react.createElement)("button", {
3262
+ key: "unarchive",
3263
+ type: "button",
3264
+ className: "dsws_actBtn",
3265
+ disabled: batch.busy,
3266
+ onClick: () => {
3267
+ runBatch("unarchive");
3268
+ }
3269
+ }, translate$1(t, "manage.batch.unarchive")),
3270
+ (0, react.createElement)("button", {
3271
+ key: "purge",
3272
+ type: "button",
3273
+ className: `dsws_actBtn dsws_btnDanger`,
3274
+ disabled: batch.busy,
3275
+ style: confirmPurge ? {
3276
+ background: "var(--dsw-alias-state-error-primary)",
3277
+ color: "var(--dsw-specific-tip)"
3278
+ } : void 0,
3279
+ onClick: () => {
3280
+ runBatch("purge");
3281
+ }
3282
+ }, confirmPurge ? translate$1(t, "manage.batch.confirm") : translate$1(t, "manage.batch.delete")),
3283
+ batch.busy && (0, react.createElement)("span", {
3284
+ key: "busy",
3285
+ className: "dsws_batchInfo"
3286
+ }, translate$1(t, "manage.batch.working"))
3287
+ ]));
3288
+ return (0, react.createElement)("div", {
3289
+ className: "dsws_manageRoot",
3290
+ style: {
3291
+ display: "flex",
3292
+ flexDirection: "column",
3293
+ flex: "1 1 auto",
3294
+ minHeight: 0
3295
+ }
3296
+ }, children);
3297
+ }
3298
+ /** Format an epoch-ms timestamp: today → HH:mm, else YYYY-MM-DD HH:mm. */
3299
+ function fmtTime$1(ms) {
3300
+ if (!ms || typeof ms !== "number") return "";
3301
+ try {
3302
+ const d = new Date(ms);
3303
+ const now = /* @__PURE__ */ new Date();
3304
+ const pad = (n) => String(n).padStart(2, "0");
3305
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
3306
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
3307
+ if (sameDay) return time;
3308
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${time}`;
3309
+ } catch {
3310
+ return "";
3311
+ }
3312
+ }
3313
+ //#endregion
3314
+ //#region src/client/panel.tsx
3315
+ /**
3316
+ * 会话管家面板壳:一个侧边栏入口,两个页签(养老院 / 体检)。
3317
+ *
3318
+ * 页签可见性由开关决定:`historyFiles=false` 时「养老院」消失,`healthCheck=false`
3319
+ * 时「体检」消失;两者都关时进入面板直接显示空壳提示(客户端也会隐藏入口)。
3320
+ */
3321
+ /** 页签 id(导出供测试断言)。 */
3322
+ const TAB_HISTORY = "dss-tab-history";
3323
+ const TAB_HEALTH = "dss-tab-health";
3324
+ const TAB_MANAGE = "dss-tab-manage";
3325
+ /** 会话管家对话框。 */
3326
+ function StewardPanel({ t, historyFiles, healthCheck, searchEnabled = false, openSession, onClose }) {
3327
+ const [tab, setTab] = (0, react.useState)(historyFiles ? "history" : healthCheck ? "health" : "manage");
3328
+ (0, react.useEffect)(() => {
3329
+ if (tab === "history" && !historyFiles && (healthCheck || searchEnabled)) setTab(healthCheck ? "health" : "manage");
3330
+ if (tab === "health" && !healthCheck && (historyFiles || searchEnabled)) setTab(historyFiles ? "history" : "manage");
3331
+ if (tab === "manage" && !searchEnabled && historyFiles) setTab("history");
3332
+ }, [
3333
+ tab,
3334
+ historyFiles,
3335
+ healthCheck,
3336
+ searchEnabled
3337
+ ]);
3338
+ (0, react.useEffect)(() => {
3339
+ const onKey = (event) => {
3340
+ if (event.key === "Escape") onClose();
3341
+ };
3342
+ document.addEventListener("keydown", onKey);
3343
+ return () => {
3344
+ document.removeEventListener("keydown", onKey);
3345
+ };
3346
+ }, [onClose]);
3347
+ const tabs = [];
3348
+ if (historyFiles) tabs.push((0, react.createElement)("button", {
3349
+ key: TAB_HISTORY,
3350
+ id: TAB_HISTORY,
3351
+ type: "button",
3352
+ className: `dss_tab${tab === "history" ? " dss_tabActive" : ""}`,
3353
+ "aria-selected": tab === "history",
3354
+ onClick: () => {
3355
+ setTab("history");
3356
+ }
3357
+ }, translate(t, "panel.tab.history")));
3358
+ if (healthCheck) tabs.push((0, react.createElement)("button", {
3359
+ key: TAB_HEALTH,
3360
+ id: TAB_HEALTH,
3361
+ type: "button",
3362
+ className: `dss_tab${tab === "health" ? " dss_tabActive" : ""}`,
3363
+ "aria-selected": tab === "health",
3364
+ onClick: () => {
3365
+ setTab("health");
3366
+ }
3367
+ }, translate(t, "panel.tab.health")));
3368
+ if (searchEnabled) tabs.push((0, react.createElement)("button", {
3369
+ key: TAB_MANAGE,
3370
+ id: TAB_MANAGE,
3371
+ type: "button",
3372
+ className: `dss_tab${tab === "manage" ? " dss_tabActive" : ""}`,
3373
+ "aria-selected": tab === "manage",
3374
+ onClick: () => {
3375
+ setTab("manage");
3376
+ }
3377
+ }, translate(t, "panel.tab.manage")));
3378
+ const body = tab === "history" && historyFiles ? (0, react.createElement)(HistoryPanel, {
3379
+ t,
3380
+ onClose
3381
+ }) : tab === "health" && healthCheck ? (0, react.createElement)(HealthPanel, {
3382
+ t,
3383
+ onClose
3384
+ }) : tab === "manage" && searchEnabled ? (0, react.createElement)("div", { style: {
3385
+ display: "flex",
3386
+ flexDirection: "column",
3387
+ flex: "1 1 auto",
3388
+ minHeight: 0
3389
+ } }, (0, react.createElement)(ManageConsole, { open: openSession ?? (() => {}) })) : (0, react.createElement)("div", { className: "dss_empty" }, translate(t, "card.enabled.desc"));
3390
+ return (0, react_dom.createPortal)((0, react.createElement)("div", { key: "steward-root" }, [(0, react.createElement)("div", {
3391
+ key: "backdrop",
3392
+ className: "dss_backdrop",
3393
+ onClick: onClose
3394
+ }), (0, react.createElement)("div", {
3395
+ key: "panel",
3396
+ className: "dss_panel",
3397
+ role: "dialog",
3398
+ "aria-label": translate(t, "panel.title"),
3399
+ style: {
3400
+ display: "flex",
3401
+ flexDirection: "column"
3402
+ }
3403
+ }, [(0, react.createElement)("div", {
3404
+ key: "head",
3405
+ className: "dss_dialogHead"
3406
+ }, [
3407
+ (0, react.createElement)("span", {
3408
+ key: "title",
3409
+ className: "dss_dialogTitle"
3410
+ }, translate(t, "panel.title")),
3411
+ (0, react.createElement)("span", {
3412
+ key: "tabs",
3413
+ className: "dss_tabRow"
3414
+ }, tabs),
3415
+ (0, react.createElement)("button", {
3416
+ key: "close",
3417
+ type: "button",
3418
+ className: "dss_actBtn",
3419
+ onClick: onClose
3420
+ }, translate(t, "panel.close"))
3421
+ ]), body])]), document.body);
1255
3422
  }
1256
3423
  /** 侧边栏脚部入口:宽栏带文案成行控件,收起轨道退化为 36x36 图标钮。 */
1257
3424
  function StewardFooter(props) {
@@ -1272,11 +3439,1421 @@ window.__ModuleLoader__.load({
1272
3439
  }, props.label) : null]);
1273
3440
  }
1274
3441
  //#endregion
3442
+ //#region src/client/search/platform.ts
3443
+ /**
3444
+ * Resolve the running platform string, three tiers deep.
3445
+ *
3446
+ * 1. UA-CH (`navigator.userAgentData.platform`) — modern and precise.
3447
+ * 2. `navigator.platform` — broadly available, deprecated, and empty in some
3448
+ * privacy modes.
3449
+ * 3. The UA string — last resort, still non-empty in practice.
3450
+ *
3451
+ * @param nav - the navigator to read; `undefined` is allowed (non-DOM host).
3452
+ * @returns a lower-cased platform string, or `''` when nothing could be read.
3453
+ */
3454
+ function detectPlatform(nav) {
3455
+ const chPlatform = nav?.userAgentData?.platform;
3456
+ if (typeof chPlatform === "string" && chPlatform !== "") return chPlatform.toLowerCase();
3457
+ const legacy = nav?.platform;
3458
+ if (typeof legacy === "string" && legacy !== "") return legacy.toLowerCase();
3459
+ const agent = nav?.userAgent;
3460
+ if (typeof agent === "string" && agent !== "") return agent.toLowerCase();
3461
+ return "";
3462
+ }
3463
+ /**
3464
+ * Derive the label vocabulary from a platform string.
3465
+ *
3466
+ * An unrecognised platform — an empty string, or a UA that names neither
3467
+ * system — takes the non-Mac vocabulary on purpose: `Ctrl` is legible to
3468
+ * everyone, while `⌘` is opaque to anyone who has never used a Mac.
3469
+ *
3470
+ * @param platform - the string produced by {@link detectPlatform}.
3471
+ * @returns the label vocabulary for that platform.
3472
+ */
3473
+ function platformLabels(platform) {
3474
+ const isMac = /mac|iphone|ipad|ipod/u.test(platform);
3475
+ const isWindows = /win/u.test(platform);
3476
+ const modLabel = isMac ? "⌘" : "Ctrl";
3477
+ return {
3478
+ platform,
3479
+ isMac,
3480
+ isWindows,
3481
+ modLabel,
3482
+ altLabel: isMac ? "⌥" : "Alt",
3483
+ shiftLabel: isMac ? "⇧" : "Shift",
3484
+ enterLabel: "↵",
3485
+ escLabel: isMac ? "esc" : "Esc",
3486
+ invokeLabel: isMac ? `${modLabel}K` : `${modLabel} K`
3487
+ };
3488
+ }
3489
+ /** Read `globalThis.navigator` without assuming a DOM (or a DOM lib). */
3490
+ function currentNavigator() {
3491
+ const candidate = globalThis.navigator;
3492
+ if (candidate === null || typeof candidate !== "object") return void 0;
3493
+ return candidate;
3494
+ }
3495
+ /** The labels for the platform this bundle is running on. */
3496
+ const LABELS = platformLabels(detectPlatform(currentNavigator()));
3497
+ /**
3498
+ * Whether a keydown event is the invoke chord advertised by
3499
+ * {@link PlatformLabels.invokeLabel}.
3500
+ *
3501
+ * The hint and the binding have to agree: a footer promising `⌘K` while the
3502
+ * handler matches `Ctrl+K` is worse than showing no hint at all. Both sides
3503
+ * call this, so neither can be changed alone. Requiring the other modifier to
3504
+ * be *absent* keeps the two chords distinct instead of letting either one
3505
+ * satisfy both platforms.
3506
+ *
3507
+ * @param event - the keydown payload.
3508
+ * @param isMac - the running platform's macOS-ness, from {@link LABELS}.
3509
+ * @returns `true` when the event is the invoke chord.
3510
+ */
3511
+ function isInvokeChord(event, isMac) {
3512
+ if (event.key !== "k" && event.key !== "K") return false;
3513
+ if (event.altKey || event.shiftKey) return false;
3514
+ return isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
3515
+ }
3516
+ //#endregion
3517
+ //#region src/client/search/panel.tsx
3518
+ /**
3519
+ * 搜索子域 client 组件(原 dsh-search-index client 半身的面板部分)。
3520
+ *
3521
+ * 注册职责(侧栏入口 / 家族 tab / 插件页配置卡)在合并包的 client/index.ts;
3522
+ * 本文件只持有:样式表、悬浮搜索面板(标题/内容双模式)、侧栏 footer 按钮、
3523
+ * 唤出和弦绑定。
3524
+ *
3525
+ * The `locale` and config services are consumed structurally: when
3526
+ * the host release lacks them the footer falls back to the bundled zh
3527
+ * dictionary and the host-composition config layer.
3528
+ */
3529
+ /** The coarse filter chips rendered above content results. */
3530
+ const CONTENT_TYPE_CHIPS = [
3531
+ {
3532
+ id: "all",
3533
+ labelKey: "filter.all"
3534
+ },
3535
+ {
3536
+ id: "user",
3537
+ labelKey: "filter.user"
3538
+ },
3539
+ {
3540
+ id: "reply",
3541
+ labelKey: "filter.reply"
3542
+ },
3543
+ {
3544
+ id: "tool",
3545
+ labelKey: "filter.tool"
3546
+ }
3547
+ ];
3548
+ /** 检索域 chips(标题/内容两模式共用)。 */
3549
+ const ARCHIVED_CHIPS = [
3550
+ {
3551
+ id: "all",
3552
+ labelKey: "domain.all"
3553
+ },
3554
+ {
3555
+ id: "active",
3556
+ labelKey: "domain.active"
3557
+ },
3558
+ {
3559
+ id: "archived",
3560
+ labelKey: "domain.archived"
3561
+ }
3562
+ ];
3563
+ /** Result-ordering chips rendered at the right of the same filter row. */
3564
+ const SORT_CHIPS = [{
3565
+ id: "relevance",
3566
+ labelKey: "sort.relevance"
3567
+ }, {
3568
+ id: "time",
3569
+ labelKey: "sort.time"
3570
+ }];
3571
+ /**
3572
+ * The identity of one content-search request, derived from every input that
3573
+ * changes the result set.
3574
+ *
3575
+ * This is the single owner of that identity. The effect that issues the
3576
+ * request and the render path that decides whether the stored result belongs
3577
+ * to the current inputs must both come through here: two hand-written copies
3578
+ * of the same template literal drift apart silently, and the render path then
3579
+ * falls through to its empty `loading` state for every query — results arrive,
3580
+ * parse, and are never shown.
3581
+ *
3582
+ * @param normalized - the trimmed query text.
3583
+ * @param contentType - the active content-type filter.
3584
+ * @param sortBy - the active result ordering.
3585
+ * @param archivedFilter - the retrieval-domain chip (all/active/archived).
3586
+ * @returns an opaque key, stable for equal inputs.
3587
+ */
3588
+ function contentRequestKey(normalized, contentType, sortBy, archivedFilter, dateRange) {
3589
+ return `${normalized}\u0000${contentType}\u0000${sortBy}\u0000${archivedFilter}\u0000${dateRange.from}\u0000${dateRange.to}`;
3590
+ }
3591
+ /**
3592
+ * Persisted ordering preference. Unlike `lastPanelMode` this one survives a
3593
+ * page reload — an ordering is a durable preference, not a session mood.
3594
+ */
3595
+ const SORT_STORE_KEY = "dsh-search-index.sortBy";
3596
+ /** Read the persisted ordering; private mode or a bad value degrades to relevance. */
3597
+ function readStoredSort() {
3598
+ try {
3599
+ return window.localStorage.getItem(SORT_STORE_KEY) === "time" ? "time" : "relevance";
3600
+ } catch {
3601
+ return "relevance";
3602
+ }
3603
+ }
3604
+ /** Persist the ordering; a storage failure still leaves this session working. */
3605
+ function writeStoredSort(next) {
3606
+ try {
3607
+ window.localStorage.setItem(SORT_STORE_KEY, next);
3608
+ } catch {}
3609
+ }
3610
+ /**
3611
+ * Local re-sort so the page honours the chosen ordering even against an older
3612
+ * host half that ignores `sortBy` and therefore omits `updatedAt` entirely —
3613
+ * in that case the host order is left untouched rather than scrambled to NaN.
3614
+ */
3615
+ function sortHits(items, sortBy) {
3616
+ if (sortBy !== "time") return [...items];
3617
+ if (!items.every((item) => typeof item.updatedAt === "number" && Number.isFinite(item.updatedAt))) return [...items];
3618
+ return [...items].sort((a, b) => b.updatedAt - a.updatedAt);
3619
+ }
3620
+ /** Last panel mode used this web session (mode memory, not persisted). */
3621
+ let lastPanelMode = "title";
3622
+ /** ------------------------------------------------------------------ styles */
3623
+ const CSS = `
3624
+ /* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
3625
+ 入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
3626
+ flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
3627
+ 其余入口(含第三方)自然落到后续行。justify-content:center 让行内所有入口
3628
+ (含不占满行的)统一居中;row-gap:0 配合入口自身 32px 高度压缩纵向占位。
3629
+ 类名用 [class*=] 中段匹配:宿主是 CSS Module 哈希类名(实测形如
3630
+ hHd-Xa_footerActions —— <hash>_<name>,哈希在前),中段跨版本稳定。 */
3631
+ [class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
3632
+ /* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
3633
+ .lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
3634
+ 42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
3635
+ 冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
3636
+ 36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
3637
+ 稳定(实测 0.56.1 / 0.60.0 规则一致)。 */
3638
+ .lc-ov-entry:not(.lc-ov-entry-rail){width:auto!important;flex:0 0 100%!important;min-width:0!important;justify-content:center!important;height:32px!important;margin:0!important;padding:0 10px!important}
3639
+ .lc-ov-entry-label{flex:0 1 auto!important}
3640
+ .dsws_root{box-sizing:border-box;position:relative;display:flex;align-items:center;justify-content:center;flex:0 0 100%;width:100%;min-width:0;container-type:inline-size}
3641
+ /* 收起轨道:回落自然宽度(根类的 100% 基准只属于宽栏形态),放弃收缩。 */
3642
+ .dsws_rootRail{flex:none;width:auto;container-type:normal}
3643
+ /* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在
3644
+ 多行堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容
3645
+ 会因不对称 padding 向左偏 1px。 */
3646
+ .dsws_button{box-sizing:border-box;display:inline-flex;align-items:center;flex:0 0 auto;min-width:0;gap:8px;height:32px;border:none;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;padding:0 10px;font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
3647
+ .dsws_buttonRail{flex:none;width:28px;height:28px;padding:0;gap:0;justify-content:center;border-radius:50%}
3648
+ .dsws_button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
3649
+ .dsws_button svg{flex:none}
3650
+ .dsws_trigger{position:fixed;z-index:2147483000;left:50%;top:50%;transform:translate(-50%,-50%);width:860px;max-width:calc(100vw - 32px);max-height:min(84vh,880px);box-sizing:border-box;background:var(--dsw-specific-tip);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;box-shadow:var(--dsw-shadow-lv3,0 8px 28px rgba(0,0,0,.16));overflow:hidden;display:flex;flex-direction:column;font-family:Inter,var(--dsw-font-family)}
3651
+ .dsws_toolrow{display:flex;align-items:center;gap:8px;padding:10px 10px 0}
3652
+ .dsws_mode{display:inline-flex;align-items:center;gap:2px;flex:none;background:var(--dsw-alias-interactive-bg-hover);border-radius:8px;padding:2px}
3653
+ .dsws_modeBtn{height:24px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:6px;padding:0 8px;font-size:12px;font-weight:500;line-height:20px}
3654
+ .dsws_modeBtn:hover{color:var(--dsw-alias-label-primary)}
3655
+ .dsws_modeBtnActive{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.08)}
3656
+ .dsws_search{flex:auto;min-width:0;height:30px;box-sizing:border-box;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;padding:0 10px;font:inherit;font-size:13px;line-height:20px}
3657
+ .dsws_search:focus{border-color:var(--dsw-alias-state-business-primary)}
3658
+ .dsws_search::placeholder{color:var(--dsw-alias-label-caption)}
3659
+ .dsws_chips{display:flex;align-items:center;gap:6px;padding:8px 10px 0;flex:none}
3660
+ .dsws_chipGap{flex:1;min-width:0}
3661
+ .dsws_sortGroup{display:flex;align-items:center;gap:6px;flex:none}
3662
+ .dsws_chip{height:24px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:999px;padding:0 10px;font-size:12px;font-weight:500;line-height:22px;white-space:nowrap}
3663
+ .dsws_chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
3664
+ .dsws_chipActive{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
3665
+ .dsws_list{flex:1 1 auto;min-height:0;overflow-y:auto;margin:8px 0 0;padding:0 6px 8px;list-style:none}
3666
+ .dsws_row{box-sizing:border-box;border-radius:8px;width:100%;padding:7px 8px;cursor:pointer;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:2px;min-width:0}
3667
+ .dsws_row:hover{background:var(--dsw-alias-interactive-bg-hover)}
3668
+ .dsws_rowTitle{display:flex;align-items:center;gap:8px;min-width:0}
3669
+ .dsws_titleText{flex:auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:18px}
3670
+ .dsws_tag{flex:none;color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;white-space:nowrap;font-variant-numeric:tabular-nums}
3671
+ .dsws_snippet{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:17px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}
3672
+ .dsws_meta{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3673
+ .dsws_status{color:var(--dsw-alias-label-tertiary);padding:10px 8px 8px;font-size:12px;line-height:18px}
3674
+ .dsws_error{color:var(--dsw-alias-state-error-primary);padding:8px;font-size:12px;line-height:18px}
3675
+ .dsws_empty{color:var(--dsw-alias-label-tertiary);padding:10px 8px 8px;font-size:12px;line-height:18px}
3676
+ .dsws_backdrop{position:fixed;inset:0;z-index:2147482999;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.24));backdrop-filter:blur(var(--dsw-mask-blur,4px))}
3677
+ .dsws_setRow{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
3678
+ .dsws_setRow:last-child{border-bottom:none}
3679
+ .dsws_setText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
3680
+ .dsws_setTitle{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
3681
+ .dsws_setDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
3682
+ .dsws_seg{display:inline-flex;align-items:center;gap:2px;background:var(--dsw-alias-interactive-bg-hover);border-radius:8px;padding:2px;flex:none}
3683
+ .dsws_segBtn{height:24px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:6px;padding:0 10px;font-size:12px;font-weight:500;line-height:20px}
3684
+ .dsws_segBtn:hover{color:var(--dsw-alias-label-primary)}
3685
+ .dsws_segBtn:disabled{cursor:not-allowed;opacity:.5}
3686
+ .dsws_segBtnActive{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.08)}
3687
+ .dsws_actBtn{height:26px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;padding:0 10px;font-size:12px;line-height:24px;white-space:nowrap}
3688
+ .dsws_actBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}
3689
+ .dsws_actBtn:disabled{cursor:not-allowed;opacity:.5}
3690
+ .dsws_btnRow{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
3691
+ .dsws_progressWrap{display:flex;flex-direction:column;gap:4px;padding:8px 10px 0}
3692
+ .dsws_progress{height:6px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}
3693
+ .dsws_progressFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-business-primary);transition:width .4s ease}
3694
+ .dsws_progressLabel{color:var(--dsw-alias-state-business-primary);font-size:12px;line-height:18px;font-weight:600;font-variant-numeric:tabular-nums}
3695
+ .dsws_panelFoot{flex:none;display:flex;align-items:center;gap:14px;padding:8px 14px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
3696
+ .dsws_footItem{display:inline-flex;align-items:center}
3697
+ .dsws_footItem>.dsws_kbd{margin-right:5px}
3698
+ .dsws_footGap{flex:1;min-width:0}
3699
+ .dsws_buttonLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3700
+ .dsws_kbd{flex:none;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-width:18px;width:auto;height:18px;padding:0 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:5px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:10px;line-height:1;white-space:nowrap;font-variant-numeric:tabular-nums}
3701
+ /*
3702
+ * The row shares its width with the session-steward entry (order 12), so at
3703
+ * narrow sidebar widths the shortcut chip, not the label, is what yields: the
3704
+ * chip is a hint repeated by the entry tooltip and the panel footer, while the
3705
+ * label is the entry's identity. Measured in Chrome: the pair needs 230px for
3706
+ * both labels plus the chip; below that the chip's 43px is what makes the
3707
+ * difference. The query is scoped to the wide form: container-type also
3708
+ * applies inline-size containment, which zeroes a flex:none rail root.
3709
+ */
3710
+ @container (max-width: 132px){.dsws_kbd{display:none}}
3711
+ .dsws_pill{flex:none;display:inline-grid;grid-template-columns:14px max-content;align-items:center;column-gap:4px;height:26px;padding:0 10px;box-sizing:border-box;border:none;border-radius:8px;font-size:12px;font-weight:500;line-height:18px;white-space:nowrap;transition:background-color 160ms ease-out,color 160ms ease-out}
3712
+ .dsws_pill .dsws_pillIcon{display:grid;place-items:center;width:14px;height:14px}
3713
+ .dsws_pill .dsws_pillLabel{display:grid;text-align:left}
3714
+ .dsws_pill .dsws_pillLabel>span{grid-area:1/1}
3715
+ .dsws_pillReady{background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary)}
3716
+ .dsws_pillWarn{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
3717
+ .dsws_pillError{background:var(--dsw-alias-state-error-tertiary,var(--dsw-alias-state-warn-tertiary));color:var(--dsw-alias-state-error-primary,var(--dsw-alias-state-warn-label))}
3718
+ .dsws_pillNeutral{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
3719
+ .dsws_pillDots span{opacity:0;animation:dsws-reveal-dot 1.5s step-end infinite}
3720
+ .dsws_pillDots span:nth-child(2){animation-delay:.5s}
3721
+ .dsws_pillDots span:nth-child(3){animation-delay:1s}
3722
+ @keyframes dsws-reveal-dot{0%,32%{opacity:0}33%,100%{opacity:1}}
3723
+ @media (prefers-reduced-motion:reduce){.dsws_pillDots span{animation:none;opacity:1}}
3724
+ `;
3725
+ /** Inject the plugin stylesheet once per activation (removed on disposal). */
3726
+ function injectStyles$1() {
3727
+ if (typeof document === "undefined") return () => {};
3728
+ if (document.querySelector("style[data-plugin-css=\"dsw-session-search-toggle/styles\"]") !== null) return () => {};
3729
+ const tag = document.createElement("style");
3730
+ tag.dataset.plugin = "dsh-search-index";
3731
+ tag.dataset.pluginCss = "dsw-session-search-toggle/styles";
3732
+ tag.textContent = CSS;
3733
+ document.head.appendChild(tag);
3734
+ return () => {
3735
+ if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
3736
+ };
3737
+ }
3738
+ /** ------------------------------------------------------------------ view */
3739
+ /** The floating search panel. */
3740
+ function SwitchPanel({ t, onClose, open }) {
3741
+ const [mode, setModeState] = (0, react.useState)(lastPanelMode);
3742
+ const setMode = (next) => {
3743
+ lastPanelMode = next;
3744
+ setModeState(next);
3745
+ };
3746
+ const [query, setQuery] = (0, react.useState)("");
3747
+ const [contentType, setContentType] = (0, react.useState)("all");
3748
+ const [archivedFilter, setArchivedFilter] = (0, react.useState)("all");
3749
+ const [dateFrom, setDateFrom] = (0, react.useState)("");
3750
+ const [dateTo, setDateTo] = (0, react.useState)("");
3751
+ const [sortBy, setSortByState] = (0, react.useState)(readStoredSort);
3752
+ const setSortBy = (next) => {
3753
+ writeStoredSort(next);
3754
+ setSortByState(next);
3755
+ };
3756
+ const [sessions, setSessions] = (0, react.useState)(null);
3757
+ const [sessionsError, setSessionsError] = (0, react.useState)(null);
3758
+ const [content, setContent] = (0, react.useState)({
3759
+ query: "",
3760
+ status: "idle",
3761
+ items: []
3762
+ });
3763
+ const [searchStatus, setSearchStatus] = (0, react.useState)({
3764
+ available: null,
3765
+ rebuilding: false,
3766
+ done: 0,
3767
+ total: 0,
3768
+ count: 0
3769
+ });
3770
+ const inputRef = (0, react.useRef)(null);
3771
+ const listRef = (0, react.useRef)(null);
3772
+ const normalized = query.trim().toLowerCase();
3773
+ (0, react.useEffect)(() => {
3774
+ let cancelled = false;
3775
+ let timer;
3776
+ const probe = () => {
3777
+ callHostAny("index-status", {}).then((res) => {
3778
+ if (cancelled) return;
3779
+ if (!res.ok) {
3780
+ setSearchStatus({
3781
+ available: false,
3782
+ reason: "unreachable",
3783
+ rebuilding: false,
3784
+ done: 0,
3785
+ total: 0,
3786
+ count: 0
3787
+ });
3788
+ return;
3789
+ }
3790
+ const status = res;
3791
+ const rebuilding = status.rebuild?.state === "building" || status.rebuild?.state === "swapping";
3792
+ setSearchStatus({
3793
+ available: status.available ?? false,
3794
+ reason: status.reason,
3795
+ rebuilding,
3796
+ done: status.rebuild?.done ?? 0,
3797
+ total: status.rebuild?.total ?? 0,
3798
+ count: status.sync?.indexed ?? 0
3799
+ });
3800
+ if (rebuilding) timer = window.setTimeout(probe, 1500);
3801
+ });
3802
+ };
3803
+ probe();
3804
+ return () => {
3805
+ cancelled = true;
3806
+ if (timer !== void 0) window.clearTimeout(timer);
3807
+ };
3808
+ }, []);
3809
+ const startIndexBuild = () => {
3810
+ setSearchStatus((prev) => ({
3811
+ ...prev,
3812
+ rebuilding: true
3813
+ }));
3814
+ callHostAny("index-rebuild", {});
3815
+ };
3816
+ (0, react.useEffect)(() => {
3817
+ if (sessions !== null) return;
3818
+ let cancelled = false;
3819
+ callHost("list-sessions", {}).then((res) => {
3820
+ if (cancelled) return;
3821
+ if (res.ok) {
3822
+ setSessions(res.items);
3823
+ setSessionsError(null);
3824
+ } else setSessionsError(res.error ?? "读取会话列表失败");
3825
+ });
3826
+ return () => {
3827
+ cancelled = true;
3828
+ };
3829
+ }, [sessions]);
3830
+ (0, react.useEffect)(() => {
3831
+ if (mode !== "content" || normalized === "") {
3832
+ if (mode !== "content") setContent({
3833
+ query: normalized,
3834
+ status: "idle",
3835
+ items: []
3836
+ });
3837
+ return;
3838
+ }
3839
+ let cancelled = false;
3840
+ const requestType = contentType;
3841
+ const requestKey = contentRequestKey(normalized, requestType, sortBy, archivedFilter, {
3842
+ from: dateFrom,
3843
+ to: dateTo
3844
+ });
3845
+ setContent((prev) => ({
3846
+ query: requestKey,
3847
+ status: "loading",
3848
+ items: prev.query === requestKey ? prev.items : []
3849
+ }));
3850
+ const timer = window.setTimeout(() => {
3851
+ callHost("content-search", {
3852
+ query: normalized,
3853
+ limit: 50,
3854
+ types: requestType === "all" ? void 0 : [requestType],
3855
+ sortBy,
3856
+ archived: archivedFilter,
3857
+ from: dateFrom === "" ? void 0 : dateFrom,
3858
+ to: dateTo === "" ? void 0 : dateTo
3859
+ }).then((res) => {
3860
+ if (cancelled) return;
3861
+ setContent({
3862
+ query: requestKey,
3863
+ status: res.ok ? "ready" : "error",
3864
+ items: res.ok ? sortHits(res.items, sortBy) : [],
3865
+ error: res.ok ? void 0 : res.error ?? "搜索失败"
3866
+ });
3867
+ });
3868
+ }, 250);
3869
+ return () => {
3870
+ cancelled = true;
3871
+ window.clearTimeout(timer);
3872
+ };
3873
+ }, [
3874
+ mode,
3875
+ normalized,
3876
+ contentType,
3877
+ sortBy,
3878
+ archivedFilter,
3879
+ dateFrom,
3880
+ dateTo
3881
+ ]);
3882
+ (0, react.useEffect)(() => {
3883
+ inputRef.current?.focus();
3884
+ }, []);
3885
+ (0, react.useEffect)(() => {
3886
+ const onKey = (e) => {
3887
+ if (e.key === "Escape") onClose();
3888
+ };
3889
+ document.addEventListener("keydown", onKey);
3890
+ return () => {
3891
+ document.removeEventListener("keydown", onKey);
3892
+ };
3893
+ }, [onClose]);
3894
+ const dateRangeMs = (0, react.useMemo)(() => {
3895
+ const parse = (value, nextDay) => {
3896
+ if (value === "") return void 0;
3897
+ const [year, month, day] = value.split("-").map(Number);
3898
+ if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return void 0;
3899
+ const date = new Date(year, month - 1, day);
3900
+ if (nextDay) date.setDate(date.getDate() + 1);
3901
+ return date.getTime();
3902
+ };
3903
+ const from = parse(dateFrom, false);
3904
+ const to = parse(dateTo, true);
3905
+ return {
3906
+ ...from === void 0 ? {} : { from },
3907
+ ...to === void 0 ? {} : { to }
3908
+ };
3909
+ }, [dateFrom, dateTo]);
3910
+ const titleRows = (0, react.useMemo)(() => {
3911
+ if (sessions === null) return [];
3912
+ const byDomain = sessions.filter((item) => archivedFilter === "active" ? item.archived !== true : archivedFilter === "archived" ? item.archived === true : true);
3913
+ const byDate = byDomain.filter((item) => (dateRangeMs.from === void 0 || item.updatedAt >= dateRangeMs.from) && (dateRangeMs.to === void 0 || item.updatedAt < dateRangeMs.to));
3914
+ if (normalized === "") return byDate;
3915
+ return byDomain.filter((item) => item.title.toLowerCase().includes(normalized) || item.cwd.toLowerCase().includes(normalized));
3916
+ }, [
3917
+ sessions,
3918
+ normalized,
3919
+ archivedFilter,
3920
+ dateRangeMs
3921
+ ]);
3922
+ const children = [];
3923
+ if (sessionsError !== null) children.push((0, react.createElement)("div", {
3924
+ key: "err",
3925
+ className: "dsws_error"
3926
+ }, translate$1(t, "panel.sessionsError", { error: sessionsError })));
3927
+ const activeRequestKey = contentRequestKey(normalized, contentType, sortBy, archivedFilter, {
3928
+ from: dateFrom,
3929
+ to: dateTo
3930
+ });
3931
+ const activeContent = content.query === activeRequestKey ? content : {
3932
+ query: activeRequestKey,
3933
+ status: "loading",
3934
+ items: []
3935
+ };
3936
+ if (mode === "title") {
3937
+ if (sessions === null) children.push((0, react.createElement)("div", {
3938
+ key: "loading",
3939
+ className: "dsws_status"
3940
+ }, translate$1(t, "panel.loadingSessions")));
3941
+ else if (titleRows.length === 0) children.push((0, react.createElement)("div", {
3942
+ key: "empty",
3943
+ className: "dsws_empty"
3944
+ }, normalized === "" ? translate$1(t, "panel.noSessions") : translate$1(t, "panel.noMatch")));
3945
+ else children.push((0, react.createElement)("ul", {
3946
+ key: "list",
3947
+ ref: listRef,
3948
+ className: "dsws_list",
3949
+ role: "listbox",
3950
+ "aria-label": translate$1(t, "panel.titleSearch")
3951
+ }, titleRows.slice(0, 200).map((item) => (0, react.createElement)("li", {
3952
+ key: item.sessionId,
3953
+ role: "option"
3954
+ }, (0, react.createElement)("button", {
3955
+ type: "button",
3956
+ className: "dsws_row",
3957
+ onClick: () => {
3958
+ open(item.sessionId);
3959
+ }
3960
+ }, [(0, react.createElement)("span", {
3961
+ key: "t",
3962
+ className: "dsws_rowTitle"
3963
+ }, [
3964
+ (0, react.createElement)("span", {
3965
+ key: "x",
3966
+ className: "dsws_titleText"
3967
+ }, item.title || translate$1(t, "panel.untitled")),
3968
+ item.archived === true && (0, react.createElement)("span", {
3969
+ key: "arch",
3970
+ className: "dsws_tagArch"
3971
+ }, translate$1(t, "tag.archived")),
3972
+ (0, react.createElement)("span", {
3973
+ key: "tag",
3974
+ className: "dsws_tag"
3975
+ }, fmtTime(item.updatedAt))
3976
+ ]), item.cwd !== "" && (0, react.createElement)("span", {
3977
+ key: "c",
3978
+ className: "dsws_meta"
3979
+ }, item.cwd)])))));
3980
+ } else if (searchStatus.rebuilding) {
3981
+ const pct = searchStatus.total > 0 ? Math.min(100, Math.round(searchStatus.done / searchStatus.total * 100)) : void 0;
3982
+ children.push((0, react.createElement)("div", {
3983
+ key: "rebuilding",
3984
+ className: "dsws_progressWrap"
3985
+ }, [(0, react.createElement)("div", {
3986
+ key: "bar",
3987
+ className: "dsws_progress"
3988
+ }, (0, react.createElement)("div", {
3989
+ className: "dsws_progressFill",
3990
+ style: pct === void 0 ? {
3991
+ width: "30%",
3992
+ opacity: .6
3993
+ } : { width: `${pct}%` }
3994
+ })), (0, react.createElement)("span", {
3995
+ key: "label",
3996
+ className: "dsws_progressLabel"
3997
+ }, translate$1(t, "panel.rebuilding", {
3998
+ done: searchStatus.done,
3999
+ total: searchStatus.total || "?"
4000
+ }))]));
4001
+ } else if (searchStatus.available === false) children.push((0, react.createElement)("div", {
4002
+ key: "unavailable",
4003
+ className: "dsws_error"
4004
+ }, [(0, react.createElement)("div", { key: "msg" }, searchStatus.reason === "unreachable" ? "索引状态不可达:Host 半可能是旧进程。请完全重启 dsh web(浏览器刷新不重载 Host)后重试。" : searchStatus.reason === "unavailable" ? translate$1(t, "panel.unavailable") : translate$1(t, "panel.notBuilt")), (0, react.createElement)("button", {
4005
+ key: "build",
4006
+ type: "button",
4007
+ className: "dsws_actBtn",
4008
+ style: { marginTop: "6px" },
4009
+ onClick: startIndexBuild
4010
+ }, translate$1(t, "panel.buildIndex"))]));
4011
+ else if (activeContent.status === "loading") children.push((0, react.createElement)("div", {
4012
+ key: "loading",
4013
+ className: "dsws_status"
4014
+ }, translate$1(t, "panel.loadingContent")));
4015
+ else if (activeContent.status === "error") children.push((0, react.createElement)("div", {
4016
+ key: "error",
4017
+ className: "dsws_error"
4018
+ }, translate$1(t, "panel.contentError", { error: activeContent.error ?? "未知错误" })));
4019
+ else if (activeContent.items.length === 0) children.push((0, react.createElement)("div", {
4020
+ key: "empty",
4021
+ className: "dsws_empty"
4022
+ }, normalized === "" ? translate$1(t, "panel.contentHint") : translate$1(t, "panel.noContent")));
4023
+ else children.push((0, react.createElement)("ul", {
4024
+ key: "list",
4025
+ ref: listRef,
4026
+ className: "dsws_list",
4027
+ role: "listbox",
4028
+ "aria-label": translate$1(t, "panel.contentSearch")
4029
+ }, activeContent.items.slice(0, 200).map((item) => (0, react.createElement)("li", {
4030
+ key: item.sessionId,
4031
+ role: "option"
4032
+ }, (0, react.createElement)("button", {
4033
+ type: "button",
4034
+ className: "dsws_row",
4035
+ onClick: () => {
4036
+ open(item.sessionId);
4037
+ }
4038
+ }, [(0, react.createElement)("span", {
4039
+ key: "t",
4040
+ className: "dsws_rowTitle"
4041
+ }, [
4042
+ (0, react.createElement)("span", {
4043
+ key: "x",
4044
+ className: "dsws_titleText"
4045
+ }, item.title || translate$1(t, "panel.untitled")),
4046
+ (0, react.createElement)("span", {
4047
+ key: "tag",
4048
+ className: "dsws_tag"
4049
+ }, typeLabel(t, item.type)),
4050
+ (0, react.createElement)("span", {
4051
+ key: "time",
4052
+ className: "dsws_tag"
4053
+ }, fmtTime(item.updatedAt))
4054
+ ]), (0, react.createElement)("span", {
4055
+ key: "s",
4056
+ className: "dsws_snippet"
4057
+ }, item.snippet || translate$1(t, "panel.noText"))])))));
4058
+ return (0, react_dom.createPortal)((0, react.createElement)("div", { key: "switch-root" }, [(0, react.createElement)("div", {
4059
+ key: "backdrop",
4060
+ className: "dsws_backdrop",
4061
+ onClick: onClose
4062
+ }), (0, react.createElement)("div", {
4063
+ key: "panel",
4064
+ className: "dsws_trigger",
4065
+ role: "dialog",
4066
+ "aria-label": translate$1(t, "card.title")
4067
+ }, [
4068
+ (0, react.createElement)("div", {
4069
+ key: "tools",
4070
+ className: "dsws_toolrow"
4071
+ }, [(0, react.createElement)("div", {
4072
+ key: "mode",
4073
+ className: "dsws_mode",
4074
+ role: "group",
4075
+ "aria-label": translate$1(t, "card.defaultMode")
4076
+ }, [(0, react.createElement)("button", {
4077
+ key: "title",
4078
+ type: "button",
4079
+ className: `dsws_modeBtn${mode === "title" ? " dsws_modeBtnActive" : ""}`,
4080
+ onClick: () => {
4081
+ setMode("title");
4082
+ }
4083
+ }, translate$1(t, "panel.titleSearch")), (0, react.createElement)("button", {
4084
+ key: "content",
4085
+ type: "button",
4086
+ className: `dsws_modeBtn${mode === "content" ? " dsws_modeBtnActive" : ""}`,
4087
+ onClick: () => {
4088
+ setMode("content");
4089
+ }
4090
+ }, translate$1(t, "panel.contentSearch"))]), (0, react.createElement)("input", {
4091
+ key: "search",
4092
+ ref: inputRef,
4093
+ className: "dsws_search",
4094
+ type: "text",
4095
+ placeholder: mode === "title" ? translate$1(t, "panel.searchTitle") : translate$1(t, "panel.searchContent"),
4096
+ value: query,
4097
+ onChange: (e) => setQuery(e.target.value)
4098
+ })]),
4099
+ (0, react.createElement)("div", {
4100
+ key: "chips",
4101
+ className: "dsws_chips",
4102
+ role: "group",
4103
+ "aria-label": translate$1(t, "domain.all")
4104
+ }, [
4105
+ ...ARCHIVED_CHIPS.map((chip) => (0, react.createElement)("button", {
4106
+ key: chip.id,
4107
+ type: "button",
4108
+ className: `dsws_chip${archivedFilter === chip.id ? " dsws_chipActive" : ""}`,
4109
+ "aria-pressed": archivedFilter === chip.id,
4110
+ onClick: () => {
4111
+ setArchivedFilter(chip.id);
4112
+ }
4113
+ }, translate$1(t, chip.labelKey))),
4114
+ (0, react.createElement)("input", {
4115
+ key: "from",
4116
+ type: "date",
4117
+ className: "dsws_search",
4118
+ style: {
4119
+ flex: "none",
4120
+ width: "128px",
4121
+ height: "24px",
4122
+ padding: "0 6px",
4123
+ fontSize: "12px"
4124
+ },
4125
+ value: dateFrom,
4126
+ title: translate$1(t, "date.from"),
4127
+ onChange: (e) => {
4128
+ setDateFrom(e.target.value);
4129
+ }
4130
+ }),
4131
+ (0, react.createElement)("input", {
4132
+ key: "to",
4133
+ type: "date",
4134
+ className: "dsws_search",
4135
+ style: {
4136
+ flex: "none",
4137
+ width: "128px",
4138
+ height: "24px",
4139
+ padding: "0 6px",
4140
+ fontSize: "12px"
4141
+ },
4142
+ value: dateTo,
4143
+ title: translate$1(t, "date.to"),
4144
+ onChange: (e) => {
4145
+ setDateTo(e.target.value);
4146
+ }
4147
+ }),
4148
+ ...mode === "content" ? CONTENT_TYPE_CHIPS.map((chip) => (0, react.createElement)("button", {
4149
+ key: chip.id,
4150
+ type: "button",
4151
+ className: `dsws_chip${contentType === chip.id ? " dsws_chipActive" : ""}`,
4152
+ "aria-pressed": contentType === chip.id,
4153
+ onClick: () => {
4154
+ setContentType(chip.id);
4155
+ }
4156
+ }, translate$1(t, chip.labelKey))) : [],
4157
+ (0, react.createElement)("span", {
4158
+ key: "gap",
4159
+ className: "dsws_chipGap"
4160
+ }),
4161
+ ...mode === "content" ? [(0, react.createElement)("span", {
4162
+ key: "sort",
4163
+ className: "dsws_sortGroup",
4164
+ role: "group",
4165
+ "aria-label": translate$1(t, "sort.label")
4166
+ }, SORT_CHIPS.map((chip) => (0, react.createElement)("button", {
4167
+ key: chip.id,
4168
+ type: "button",
4169
+ className: `dsws_chip${sortBy === chip.id ? " dsws_chipActive" : ""}`,
4170
+ "aria-pressed": sortBy === chip.id,
4171
+ title: chip.id === "time" ? translate$1(t, "sort.time.hint") : translate$1(t, "sort.relevance.hint"),
4172
+ onClick: () => {
4173
+ setSortBy(chip.id);
4174
+ }
4175
+ }, translate$1(t, chip.labelKey))))] : []
4176
+ ]),
4177
+ children,
4178
+ (0, react.createElement)("div", {
4179
+ key: "foot",
4180
+ className: "dsws_panelFoot"
4181
+ }, [
4182
+ (0, react.createElement)("span", {
4183
+ key: "index",
4184
+ className: "dsws_footItem"
4185
+ }, [(0, react.createElement)("span", { key: "label" }, searchStatus.rebuilding ? translate$1(t, "panel.index.rebuilding", {
4186
+ done: searchStatus.done,
4187
+ total: searchStatus.total || "?"
4188
+ }) : searchStatus.available === false ? translate$1(t, "panel.index.none") : translate$1(t, "panel.index.count", { n: searchStatus.count })), (0, react.createElement)("button", {
4189
+ key: "rebuild",
4190
+ type: "button",
4191
+ className: "dsws_actBtn",
4192
+ style: {
4193
+ height: "20px",
4194
+ padding: "0 8px",
4195
+ fontSize: "11px",
4196
+ lineHeight: "18px"
4197
+ },
4198
+ disabled: searchStatus.rebuilding,
4199
+ title: translate$1(t, "card.index.rebuild.hint"),
4200
+ onClick: startIndexBuild
4201
+ }, searchStatus.rebuilding ? translate$1(t, "card.index.rebuilding.btn") : translate$1(t, "card.index.rebuild"))]),
4202
+ (0, react.createElement)("span", {
4203
+ key: "invoke",
4204
+ className: "dsws_footItem"
4205
+ }, [(0, react.createElement)("kbd", {
4206
+ key: "k",
4207
+ className: "dsws_kbd"
4208
+ }, LABELS.invokeLabel), translate$1(t, "panel.footer.invoke")]),
4209
+ (0, react.createElement)("span", {
4210
+ key: "gap",
4211
+ className: "dsws_footGap"
4212
+ }),
4213
+ (0, react.createElement)("span", {
4214
+ key: "close",
4215
+ className: "dsws_footItem"
4216
+ }, [(0, react.createElement)("kbd", {
4217
+ key: "k",
4218
+ className: "dsws_kbd"
4219
+ }, LABELS.escLabel), translate$1(t, "panel.footer.close")])
4220
+ ])
4221
+ ])]), document.body);
4222
+ }
4223
+ /**
4224
+ * Whether the user has the sidebar entry switched on.
4225
+ *
4226
+ * The `enabled` field has existed since the card shipped, but nothing read it:
4227
+ * the switch promised "show the search entry at the bottom of the sidebar" and
4228
+ * controlled nothing at all. Reading it here is the whole fix.
4229
+ *
4230
+ * An absent or unreadable scope keeps the entry visible — a settings service we
4231
+ * cannot read is not a user asking for the feature off, and hiding the entry
4232
+ * would also hide the only way back to the panel.
4233
+ *
4234
+ * @param scope - the bound `switch-search` namespace scope, when available.
4235
+ * @returns `false` only when a readable scope says the entry is switched off.
4236
+ */
4237
+ function useEntryEnabled(scope) {
4238
+ return (0, react.useSyncExternalStore)((listener) => scope?.subscribe(listener) ?? (() => {}), () => scope?.getSnapshot())?.value?.enabled !== false;
4239
+ }
4240
+ /** The footer entry: one icon button that opens the search panel. */
4241
+ function SwitchFooter({ t, wide, open, scope }) {
4242
+ const [openPanel, setOpenPanel] = (0, react.useState)(false);
4243
+ const enabled = useEntryEnabled(scope);
4244
+ (0, react.useEffect)(() => {
4245
+ if (!enabled) return void 0;
4246
+ const onKey = (event) => {
4247
+ if (!isInvokeChord(event, LABELS.isMac)) return;
4248
+ event.preventDefault();
4249
+ setOpenPanel(true);
4250
+ };
4251
+ document.addEventListener("keydown", onKey);
4252
+ return () => {
4253
+ document.removeEventListener("keydown", onKey);
4254
+ };
4255
+ }, [enabled]);
4256
+ if (!enabled) return null;
4257
+ return (0, react.createElement)("div", { className: wide ? "dsws_root" : "dsws_root dsws_rootRail" }, [(0, react.createElement)("button", {
4258
+ key: "btn",
4259
+ type: "button",
4260
+ className: wide ? "dsws_button" : "dsws_button dsws_buttonRail",
4261
+ title: `${translate$1(t, "panel.entry")}(${LABELS.invokeLabel})`,
4262
+ "aria-label": `${translate$1(t, "panel.entry")}(${translate$1(t, "panel.titleSearch")} / ${translate$1(t, "panel.contentSearch")},${LABELS.invokeLabel})`,
4263
+ "aria-expanded": openPanel,
4264
+ onClick: () => {
4265
+ setOpenPanel(true);
4266
+ }
4267
+ }, [
4268
+ searchIcon(),
4269
+ wide && (0, react.createElement)("span", {
4270
+ key: "label",
4271
+ className: "dsws_buttonLabel"
4272
+ }, translate$1(t, "panel.entry")),
4273
+ wide && (0, react.createElement)("kbd", {
4274
+ key: "key",
4275
+ className: "dsws_kbd"
4276
+ }, LABELS.invokeLabel)
4277
+ ]), openPanel && (0, react.createElement)(SwitchPanel, {
4278
+ key: "panel",
4279
+ t,
4280
+ onClose: () => {
4281
+ setOpenPanel(false);
4282
+ },
4283
+ open: (sessionId) => {
4284
+ setOpenPanel(false);
4285
+ open(sessionId);
4286
+ }
4287
+ })]);
4288
+ }
4289
+ /** ------------------------------------------------------------------ helpers */
4290
+ /** Format an epoch-ms timestamp: today → HH:mm, else YYYY-MM-DD HH:mm. */
4291
+ function fmtTime(ms) {
4292
+ if (!ms || typeof ms !== "number") return "";
4293
+ try {
4294
+ const d = new Date(ms);
4295
+ const now = /* @__PURE__ */ new Date();
4296
+ const pad = (n) => String(n).padStart(2, "0");
4297
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
4298
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
4299
+ if (sameDay) return time;
4300
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${time}`;
4301
+ } catch {
4302
+ return "";
4303
+ }
4304
+ }
4305
+ /** Short label for a content-hit event type. */
4306
+ function typeLabel(t, type) {
4307
+ if (type === "user/message" || type === "assistant/message" || type === "tool/call" || type === "tool/result") return translate$1(t, `type.${type}`);
4308
+ return type;
4309
+ }
4310
+ /** Inline search icon (stroke aligned with the product's 1.75 hairline). */
4311
+ function searchIcon() {
4312
+ return (0, react.createElement)("svg", {
4313
+ width: 14,
4314
+ height: 14,
4315
+ viewBox: "0 0 16 16",
4316
+ fill: "none",
4317
+ "aria-hidden": true
4318
+ }, (0, react.createElement)("circle", {
4319
+ cx: 7,
4320
+ cy: 7,
4321
+ r: 4.5,
4322
+ stroke: "currentColor",
4323
+ strokeWidth: 1.75,
4324
+ fill: "none"
4325
+ }), (0, react.createElement)("path", {
4326
+ d: "M10.5 10.5 L14 14",
4327
+ stroke: "currentColor",
4328
+ strokeWidth: 1.75,
4329
+ strokeLinecap: "round"
4330
+ }));
4331
+ }
4332
+ //#endregion
4333
+ //#region src/client/search/card.tsx
4334
+ /**
4335
+ * Session-search settings card(0.1.7 起不再挂载:设置表单由 host 侧 .volatile() 字段自动生成)。
4336
+ * plugin, following the dsh-thinking-levels card pattern.
4337
+ *
4338
+ * The card binds the `switch-search` settings namespace through the
4339
+ * `settingsScope` cordis service and renders its fields as one editable card:
4340
+ * the enable switch, the default panel mode, the independent-index sync
4341
+ * knobs, and the index-lifecycle block (status, the non-destructive 整理
4342
+ * button, and the JSON snapshot export/import seam). Every change commits
4343
+ * immediately through the scope (no staged form).
4344
+ *
4345
+ * Kept dependency-free beyond react: the scope is subscribed with
4346
+ * `useSyncExternalStore`, and the controls are plain HTML reusing the
4347
+ * stylesheet the client half injects.
4348
+ */
4349
+ /** Row shared by every field of the card. */
4350
+ function Row(props) {
4351
+ return (0, react.createElement)("div", { className: "dsws_setRow" }, [(0, react.createElement)("div", {
4352
+ key: "text",
4353
+ className: "dsws_setText"
4354
+ }, [(0, react.createElement)("span", {
4355
+ key: "t",
4356
+ className: "dsws_setTitle"
4357
+ }, props.title), props.desc !== void 0 && (0, react.createElement)("span", {
4358
+ key: "d",
4359
+ className: "dsws_setDesc"
4360
+ }, props.desc)]), (0, react.createElement)("div", { key: "ctl" }, props.control)]);
4361
+ }
4362
+ /**
4363
+ * A boolean switch editing one namespace field.
4364
+ *
4365
+ * Drawn entirely with inline styles (thinking-levels discipline): the card
4366
+ * must not depend on the injected stylesheet — scoped or late-loaded settings
4367
+ * pages left the class-based switch rendering as a bare checkbox dot.
4368
+ */
4369
+ function Toggle(props) {
4370
+ const checked = props.checked;
4371
+ return (0, react.createElement)("label", { style: {
4372
+ position: "relative",
4373
+ width: "40px",
4374
+ height: "22px",
4375
+ flex: "none",
4376
+ display: "inline-block",
4377
+ cursor: props.writable ? "pointer" : "not-allowed"
4378
+ } }, [
4379
+ (0, react.createElement)("input", {
4380
+ key: "input",
4381
+ type: "checkbox",
4382
+ checked,
4383
+ disabled: !props.writable,
4384
+ onChange: (e) => props.onChange(e.target.checked),
4385
+ style: {
4386
+ position: "absolute",
4387
+ inset: 0,
4388
+ width: "100%",
4389
+ height: "100%",
4390
+ opacity: 0,
4391
+ margin: 0,
4392
+ cursor: props.writable ? "pointer" : "not-allowed"
4393
+ }
4394
+ }),
4395
+ (0, react.createElement)("span", {
4396
+ key: "track",
4397
+ style: {
4398
+ position: "absolute",
4399
+ inset: 0,
4400
+ background: checked ? "var(--dsw-alias-state-business-primary, #4c6ef5)" : "var(--dsw-alias-bg-module-platform, rgba(127,127,127,0.25))",
4401
+ border: `1px solid ${checked ? "var(--dsw-alias-state-business-primary, #4c6ef5)" : "var(--dsw-alias-border-l2, rgba(127,127,127,0.35))"}`,
4402
+ borderRadius: "11px",
4403
+ transition: "background .15s ease, border-color .15s ease",
4404
+ pointerEvents: "none"
4405
+ }
4406
+ }),
4407
+ (0, react.createElement)("span", {
4408
+ key: "thumb",
4409
+ style: {
4410
+ position: "absolute",
4411
+ top: "2px",
4412
+ left: "2px",
4413
+ width: "16px",
4414
+ height: "16px",
4415
+ background: "#fff",
4416
+ borderRadius: "50%",
4417
+ boxShadow: "0 1px 2px rgba(0,0,0,.2)",
4418
+ transition: "transform .15s ease",
4419
+ transform: checked ? "translateX(18px)" : "none",
4420
+ pointerEvents: "none"
4421
+ }
4422
+ })
4423
+ ]);
4424
+ }
4425
+ /**
4426
+ * A status pill in the official ConnectionIndicator visual language: rounded
4427
+ * chip, semantic state tokens, animated dots while syncing.
4428
+ */
4429
+ function StatusPill(props) {
4430
+ const stateClass = props.state === "ready" ? "dsws_pillReady" : props.state === "syncing" ? "dsws_pillWarn" : props.state === "error" ? "dsws_pillError" : "dsws_pillNeutral";
4431
+ return (0, react.createElement)("span", { className: `dsws_pill ${stateClass}` }, [(0, react.createElement)("span", {
4432
+ key: "icon",
4433
+ className: "dsws_pillIcon",
4434
+ "aria-hidden": true
4435
+ }, props.icon), (0, react.createElement)("span", {
4436
+ key: "label",
4437
+ className: "dsws_pillLabel"
4438
+ }, [(0, react.createElement)("span", { key: "text" }, props.label), props.dots === true && (0, react.createElement)("span", {
4439
+ key: "dots",
4440
+ className: "dsws_pillDots",
4441
+ "aria-hidden": true
4442
+ }, (0, react.createElement)("span", {}, "."), (0, react.createElement)("span", {}, "."), (0, react.createElement)("span", {}, "."))])]);
4443
+ }
4444
+ /** The independent-index lifecycle block (status + 整理 + snapshot seam). */
4445
+ function IndexBlock(props) {
4446
+ const [status, setStatus] = (0, react.useState)(null);
4447
+ const [fetchError, setFetchError] = (0, react.useState)(null);
4448
+ const [attempt, setAttempt] = (0, react.useState)(0);
4449
+ const [note, setNote] = (0, react.useState)(null);
4450
+ const [busy, setBusy] = (0, react.useState)(false);
4451
+ (0, react.useEffect)(() => {
4452
+ let cancelled = false;
4453
+ let timer;
4454
+ const refresh = () => {
4455
+ callHostAny("index-status", {}).then((res) => {
4456
+ if (cancelled) return;
4457
+ if (res.ok) {
4458
+ setStatus(res);
4459
+ setFetchError(null);
4460
+ } else {
4461
+ setStatus(null);
4462
+ setFetchError(res.error ?? "index-status 请求失败");
4463
+ }
4464
+ if (res.rebuild?.state === "building" || res.rebuild?.state === "swapping") timer = window.setTimeout(refresh, 2e3);
4465
+ else setBusy(false);
4466
+ });
4467
+ };
4468
+ refresh();
4469
+ return () => {
4470
+ cancelled = true;
4471
+ if (timer !== void 0) window.clearTimeout(timer);
4472
+ };
4473
+ }, [attempt]);
4474
+ const t = props.t;
4475
+ const rebuilding = status?.rebuild?.state === "building" || status?.rebuild?.state === "swapping";
4476
+ const rebuildError = status?.rebuild?.state === "error" ? status.rebuild.error : void 0;
4477
+ const syncFailures = status?.sync?.failures?.length ?? 0;
4478
+ const blocking = busy || rebuilding;
4479
+ const onRebuild = () => {
4480
+ setBusy(true);
4481
+ setNote(null);
4482
+ callHostAny("index-rebuild", {}).then((res) => {
4483
+ if (!res.ok) {
4484
+ setNote(translate$1(t, "card.action.failed", { error: res.error ?? "?" }));
4485
+ setBusy(false);
4486
+ }
4487
+ });
4488
+ };
4489
+ const onExport = () => {
4490
+ setBusy(true);
4491
+ setNote(null);
4492
+ downloadSnapshot().then(() => setNote(translate$1(t, "card.index.exported"))).catch((err) => setNote(translate$1(t, "card.action.failed", { error: String(err instanceof Error ? err.message : err) }))).finally(() => setBusy(false));
4493
+ };
4494
+ const onImportFile = (file) => {
4495
+ setBusy(true);
4496
+ setNote(null);
4497
+ file.text().then((text) => callHostAny("index-import", text, 3e4)).then((res) => {
4498
+ if (res.ok) setNote(translate$1(t, "card.index.import.started"));
4499
+ else setNote(res.error ?? translate$1(t, "card.index.importParse"));
4500
+ }).catch((err) => setNote(translate$1(t, "card.action.failed", { error: String(err instanceof Error ? err.message : err) }))).finally(() => setBusy(false));
4501
+ };
4502
+ const pillState = rebuilding ? "syncing" : status?.rebuild?.state === "error" || fetchError !== null ? "error" : status === null || status.available !== true ? "neutral" : "ready";
4503
+ const pillLabel = fetchError !== null ? "状态读取失败" : rebuilding ? translate$1(t, "card.index.rebuilding", {
4504
+ done: status?.rebuild?.done ?? 0,
4505
+ total: status?.rebuild?.total || "?"
4506
+ }) : status === null ? translate$1(t, "card.index.reading") : status.available === true ? translate$1(t, "card.index.desc", { indexed: status.sync?.indexed ?? "?" }) : translate$1(t, "card.index.empty");
4507
+ const statusLine = fetchError !== null ? `索引状态读取失败:${fetchError}。请完全重启 dsh web(浏览器刷新不会重载 Host 半)后重试。` : status === null ? translate$1(t, "card.index.reading") : rebuilding ? translate$1(t, "card.index.rebuilding", {
4508
+ done: status.rebuild?.done ?? 0,
4509
+ total: status.rebuild?.total || "?"
4510
+ }) : status.available === true ? `${translate$1(t, "card.index.desc", { indexed: status.sync?.indexed ?? "?" })}${(status.archives?.length ?? 0) > 0 ? translate$1(t, "card.index.archives", { archives: status.archives?.length }) : ""}` : translate$1(t, "card.index.empty");
4511
+ const rebuildDone = status?.rebuild?.done ?? 0;
4512
+ const rebuildTotal = status?.rebuild?.total ?? 0;
4513
+ const progressPct = rebuilding && rebuildTotal > 0 ? Math.min(100, Math.round(rebuildDone / rebuildTotal * 100)) : void 0;
4514
+ const progressBlock = rebuilding ? (0, react.createElement)("div", {
4515
+ key: "progress",
4516
+ style: {
4517
+ display: "flex",
4518
+ flexDirection: "column",
4519
+ gap: "4px",
4520
+ margin: "2px 0 6px",
4521
+ width: "100%"
4522
+ }
4523
+ }, [(0, react.createElement)("div", {
4524
+ key: "bar",
4525
+ style: {
4526
+ height: "6px",
4527
+ borderRadius: "3px",
4528
+ background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.2))",
4529
+ overflow: "hidden"
4530
+ }
4531
+ }, (0, react.createElement)("div", { style: progressPct === void 0 ? {
4532
+ height: "100%",
4533
+ width: "30%",
4534
+ borderRadius: "3px",
4535
+ background: "var(--dsw-alias-state-business-primary, #4c6ef5)",
4536
+ opacity: .6
4537
+ } : {
4538
+ height: "100%",
4539
+ width: `${progressPct}%`,
4540
+ borderRadius: "3px",
4541
+ background: "var(--dsw-alias-state-business-primary, #4c6ef5)",
4542
+ transition: "width .4s ease"
4543
+ } })), (0, react.createElement)("span", { style: {
4544
+ color: "var(--dsw-alias-state-business-primary, #4c6ef5)",
4545
+ fontSize: "12px",
4546
+ lineHeight: "18px",
4547
+ fontWeight: 600,
4548
+ fontVariantNumeric: "tabular-nums"
4549
+ } }, progressPct === void 0 ? translate$1(t, "card.index.rebuilding", {
4550
+ done: rebuildDone,
4551
+ total: "?"
4552
+ }) : `${progressPct}% · ${translate$1(t, "card.index.rebuilding", {
4553
+ done: rebuildDone,
4554
+ total: rebuildTotal
4555
+ })}`)]) : null;
4556
+ return (0, react.createElement)("div", {
4557
+ className: "dsws_setRow",
4558
+ style: rebuilding ? {
4559
+ flexDirection: "column",
4560
+ alignItems: "stretch"
4561
+ } : void 0
4562
+ }, [
4563
+ (0, react.createElement)("div", {
4564
+ key: "text",
4565
+ className: "dsws_setText"
4566
+ }, [
4567
+ (0, react.createElement)("span", {
4568
+ key: "t",
4569
+ className: "dsws_setTitle"
4570
+ }, translate$1(t, "card.index")),
4571
+ (0, react.createElement)("span", {
4572
+ key: "p",
4573
+ style: {
4574
+ display: "flex",
4575
+ gap: "8px",
4576
+ alignItems: "center",
4577
+ flexWrap: "wrap"
4578
+ }
4579
+ }, [(0, react.createElement)(StatusPill, {
4580
+ key: "pill",
4581
+ state: pillState,
4582
+ label: pillLabel,
4583
+ icon: pillState === "ready" ? "✓" : pillState === "syncing" || pillState === "error" ? "!" : "·",
4584
+ dots: pillState === "syncing"
4585
+ }), (status?.archivedSessions ?? 0) > 0 && (0, react.createElement)("span", {
4586
+ key: "archived",
4587
+ className: "dsws_pill dsws_pillNeutral"
4588
+ }, translate$1(t, "panel.archived", { count: status?.archivedSessions ?? 0 }))]),
4589
+ (0, react.createElement)("span", {
4590
+ key: "d",
4591
+ className: "dsws_setDesc"
4592
+ }, statusLine),
4593
+ (status?.archivedSessions ?? 0) > 0 && (0, react.createElement)("span", {
4594
+ key: "archHint",
4595
+ className: "dsws_setDesc"
4596
+ }, status?.steward === "missing" ? translate$1(t, "card.index.archivedMissing") : translate$1(t, "card.index.archivedOwner")),
4597
+ rebuildError !== null && rebuildError !== void 0 && (0, react.createElement)("span", {
4598
+ key: "err",
4599
+ className: "dsws_setDesc"
4600
+ }, translate$1(t, "card.index.rebuildError", { error: rebuildError })),
4601
+ syncFailures > 0 && (0, react.createElement)("span", {
4602
+ key: "warn",
4603
+ className: "dsws_setDesc"
4604
+ }, translate$1(t, "card.index.failures", { count: syncFailures })),
4605
+ note !== null && (0, react.createElement)("span", {
4606
+ key: "note",
4607
+ className: "dsws_setDesc"
4608
+ }, note),
4609
+ (0, react.createElement)("span", {
4610
+ key: "hint",
4611
+ className: "dsws_setDesc"
4612
+ }, translate$1(t, "card.index.rebuild.hint"))
4613
+ ]),
4614
+ progressBlock,
4615
+ (0, react.createElement)("div", {
4616
+ key: "btns",
4617
+ className: "dsws_btnRow"
4618
+ }, [
4619
+ (0, react.createElement)("button", {
4620
+ key: "rebuild",
4621
+ type: "button",
4622
+ className: "dsws_actBtn",
4623
+ disabled: blocking,
4624
+ onClick: onRebuild
4625
+ }, rebuilding ? translate$1(t, "card.index.rebuilding.btn") : translate$1(t, "card.index.rebuild")),
4626
+ (0, react.createElement)("button", {
4627
+ key: "export",
4628
+ type: "button",
4629
+ className: "dsws_actBtn",
4630
+ disabled: blocking || status?.available !== true,
4631
+ onClick: onExport
4632
+ }, translate$1(t, "card.index.export")),
4633
+ fetchError !== null && (0, react.createElement)("button", {
4634
+ key: "retryStatus",
4635
+ type: "button",
4636
+ className: "dsws_actBtn",
4637
+ onClick: () => {
4638
+ setAttempt((n) => n + 1);
4639
+ }
4640
+ }, "重试状态"),
4641
+ (0, react.createElement)("label", {
4642
+ key: "import",
4643
+ className: "dsws_actBtn"
4644
+ }, [translate$1(t, "card.index.import"), (0, react.createElement)("input", {
4645
+ key: "file",
4646
+ type: "file",
4647
+ accept: ".jsonl,.json,text/plain,application/json",
4648
+ style: { display: "none" },
4649
+ onChange: (e) => {
4650
+ const file = e.target.files?.[0] ?? void 0;
4651
+ e.target.value = "";
4652
+ if (file !== void 0 && file !== null) onImportFile(file);
4653
+ }
4654
+ })])
4655
+ ])
4656
+ ]);
4657
+ }
4658
+ /**
4659
+ * The settings card body: a collapsed drawer shell (title + description +
4660
+ * chevron, thinking-levels pattern) expanding into the namespace fields and
4661
+ * the index-lifecycle block.
4662
+ * @param props - locale seat (optional) and the bound namespace scope.
4663
+ */
4664
+ function SearchSettingsCard(props) {
4665
+ const { scope } = props;
4666
+ const t = props.t;
4667
+ const [open, setOpen] = (0, react.useState)(true);
4668
+ const body = createCardBody({
4669
+ t,
4670
+ snapshot: (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot()),
4671
+ scope
4672
+ });
4673
+ return (0, react.createElement)("div", { style: {
4674
+ border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))",
4675
+ background: "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))",
4676
+ borderRadius: "12px",
4677
+ transition: "border-color 0.16s, background 0.16s"
4678
+ } }, [(0, react.createElement)("button", {
4679
+ key: "head",
4680
+ type: "button",
4681
+ "aria-expanded": open,
4682
+ style: {
4683
+ appearance: "none",
4684
+ width: "100%",
4685
+ font: "inherit",
4686
+ color: "inherit",
4687
+ textAlign: "left",
4688
+ cursor: "pointer",
4689
+ background: "none",
4690
+ border: 0,
4691
+ borderRadius: "12px",
4692
+ display: "flex",
4693
+ alignItems: "center",
4694
+ gap: "12px",
4695
+ padding: "14px 16px"
4696
+ },
4697
+ onClick: () => {
4698
+ setOpen((current) => !current);
4699
+ }
4700
+ }, [(0, react.createElement)("span", {
4701
+ key: "text",
4702
+ style: {
4703
+ flex: "1 1 0%",
4704
+ minWidth: 0
4705
+ }
4706
+ }, [(0, react.createElement)("div", {
4707
+ key: "title",
4708
+ style: {
4709
+ fontSize: "14px",
4710
+ fontWeight: 600,
4711
+ color: "var(--dsw-alias-label-primary)"
4712
+ }
4713
+ }, translate$1(t, "card.title")), (0, react.createElement)("div", {
4714
+ key: "desc",
4715
+ style: {
4716
+ color: "var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))",
4717
+ fontSize: "13px",
4718
+ lineHeight: 1.5
4719
+ }
4720
+ }, translate$1(t, "card.description"))]), (0, react.createElement)("svg", {
4721
+ key: "chev",
4722
+ width: 16,
4723
+ height: 16,
4724
+ viewBox: "0 0 16 16",
4725
+ "aria-hidden": true,
4726
+ style: {
4727
+ color: "var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))",
4728
+ flex: "0 0 auto",
4729
+ transition: "transform 0.16s",
4730
+ transform: open ? "rotate(180deg)" : "none"
4731
+ }
4732
+ }, (0, react.createElement)("path", {
4733
+ d: "M4 6l4 4 4-4",
4734
+ fill: "none",
4735
+ stroke: "currentColor",
4736
+ strokeWidth: 1.5,
4737
+ strokeLinecap: "round",
4738
+ strokeLinejoin: "round"
4739
+ }))]), open && (0, react.createElement)("div", {
4740
+ key: "body",
4741
+ style: { padding: "12px 16px" }
4742
+ }, body)]);
4743
+ }
4744
+ /** The card's expandable content: namespace fields plus the index block. */
4745
+ function createCardBody(props) {
4746
+ const t = props.t;
4747
+ const snapshot = props.snapshot;
4748
+ const scope = props.scope;
4749
+ if (snapshot.status === "unavailable") return [(0, react.createElement)("div", {
4750
+ className: "dsws_setRow",
4751
+ key: "unavailable"
4752
+ }, (0, react.createElement)("span", { className: "dsws_setTitle" }, translate$1(t, "card.unavailable")))];
4753
+ const value = snapshot.value ?? {};
4754
+ const writable = snapshot.writable;
4755
+ const syncIntervalSeconds = Math.round((value.syncIntervalMs ?? 3e4) / 1e3);
4756
+ const archiveKeep = value.archiveKeep ?? 2;
4757
+ const children = [
4758
+ (0, react.createElement)(Row, {
4759
+ key: "enable",
4760
+ title: translate$1(t, "card.enabled"),
4761
+ desc: translate$1(t, "card.enabled.desc"),
4762
+ control: (0, react.createElement)(Toggle, {
4763
+ checked: value.enabled ?? true,
4764
+ writable,
4765
+ onChange: (checked) => {
4766
+ scope.set("enabled", checked);
4767
+ }
4768
+ })
4769
+ }),
4770
+ (0, react.createElement)(Row, {
4771
+ key: "mode",
4772
+ title: translate$1(t, "card.defaultMode"),
4773
+ desc: translate$1(t, "card.defaultMode.desc"),
4774
+ control: (0, react.createElement)("div", {
4775
+ className: "dsws_seg",
4776
+ role: "group"
4777
+ }, ["title", "content"].map((mode) => (0, react.createElement)("button", {
4778
+ key: mode,
4779
+ type: "button",
4780
+ className: `dsws_segBtn${(value.defaultMode ?? "title") === mode ? " dsws_segBtnActive" : ""}`,
4781
+ "aria-pressed": (value.defaultMode ?? "title") === mode,
4782
+ disabled: !writable,
4783
+ onClick: () => {
4784
+ scope.set("defaultMode", mode);
4785
+ }
4786
+ }, mode === "title" ? translate$1(t, "card.mode.title") : translate$1(t, "card.mode.content"))))
4787
+ }),
4788
+ (0, react.createElement)(Row, {
4789
+ key: "autoSync",
4790
+ title: translate$1(t, "card.autoSync"),
4791
+ desc: translate$1(t, "card.autoSync.desc"),
4792
+ control: (0, react.createElement)(Toggle, {
4793
+ checked: value.autoSync ?? true,
4794
+ writable,
4795
+ onChange: (checked) => {
4796
+ scope.set("autoSync", checked);
4797
+ }
4798
+ })
4799
+ }),
4800
+ (0, react.createElement)(Row, {
4801
+ key: "interval",
4802
+ title: translate$1(t, "card.syncInterval"),
4803
+ desc: translate$1(t, "card.syncInterval.desc"),
4804
+ control: (0, react.createElement)("input", {
4805
+ type: "number",
4806
+ min: 5,
4807
+ max: 3600,
4808
+ disabled: !writable,
4809
+ value: syncIntervalSeconds,
4810
+ style: {
4811
+ width: "72px",
4812
+ boxSizing: "border-box"
4813
+ },
4814
+ className: "dsws_search",
4815
+ onChange: (e) => {
4816
+ const seconds = Number(e.target.value);
4817
+ if (Number.isFinite(seconds) && seconds >= 5) scope.set("syncIntervalMs", Math.round(seconds * 1e3));
4818
+ }
4819
+ })
4820
+ }),
4821
+ (0, react.createElement)(Row, {
4822
+ key: "archiveKeep",
4823
+ title: translate$1(t, "card.archiveKeep"),
4824
+ desc: translate$1(t, "card.archiveKeep.desc"),
4825
+ control: (0, react.createElement)("input", {
4826
+ type: "number",
4827
+ min: 0,
4828
+ max: 20,
4829
+ disabled: !writable,
4830
+ value: archiveKeep,
4831
+ style: {
4832
+ width: "72px",
4833
+ boxSizing: "border-box"
4834
+ },
4835
+ className: "dsws_search",
4836
+ onChange: (e) => {
4837
+ const count = Number(e.target.value);
4838
+ if (Number.isFinite(count) && count >= 0) scope.set("archiveKeep", Math.round(count));
4839
+ }
4840
+ })
4841
+ })
4842
+ ];
4843
+ const indexBlock = IndexBlock({ t });
4844
+ children.push(indexBlock);
4845
+ if (!writable) children.push((0, react.createElement)("div", {
4846
+ key: "ro",
4847
+ className: "dsws_setDesc"
4848
+ }, translate$1(t, "card.readonly")));
4849
+ return children;
4850
+ }
4851
+ //#endregion
1275
4852
  //#region src/client/index.ts
1276
4853
  /** 字典命名空间(locale.register 用)。 */
1277
4854
  const NS = "dsh-manage-session";
1278
4855
  /** 客户端插件声明的注入面。 */
1279
- const inject = ["slots"];
4856
+ const inject = ["slots", "locale"];
1280
4857
  /** 注入样式(幂等)。 */
1281
4858
  function injectStyles() {
1282
4859
  const id = "dsh-manage-session-styles";
@@ -1285,23 +4862,37 @@ window.__ModuleLoader__.load({
1285
4862
  const style = document.createElement("style");
1286
4863
  style.id = id;
1287
4864
  style.textContent = `
1288
- /* 侧栏 footer 槽位公约(2026-09-26):一行多入口(第三方 dsh-context 等)会互相
1289
- 挤占 —— 宿主 .footerActions 是 nowrap flex 行。这里允许容器换行,并把本插件
1290
- 入口钉成独占一整行(flex-basis:100%);其余入口(含第三方)自然落到后续行,
1291
- 各行内部自行布局,谁也不挤谁。类名用 [class*=] 中段匹配:宿主是 CSS Module
1292
- 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
4865
+ /* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
4866
+ 入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
4867
+ flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
4868
+ 其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
4869
+ justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
4870
+ 配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
4871
+ Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
1293
4872
  中段跨版本稳定。 */
1294
- [class*="footerActions"]{flex-wrap:wrap;row-gap:2px;height:auto;min-height:0}
4873
+ [class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
4874
+ /* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
4875
+ .lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
4876
+ 42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
4877
+ 冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
4878
+ 36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
4879
+ 稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
4880
+ 任一被禁用时矫正仍生效。 */
4881
+ .lc-ov-entry:not(.lc-ov-entry-rail){width:auto!important;flex:0 0 100%!important;min-width:0!important;justify-content:center!important;height:32px!important;margin:0!important;padding:0 10px!important}
4882
+ .lc-ov-entry-label{flex:0 1 auto!important}
1295
4883
  /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
1296
4884
  .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
1297
4885
  .dss_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
1298
- .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:42px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
4886
+ /* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
4887
+ 堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
4888
+ 不对称 padding 向左偏 1px。 */
4889
+ .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 10px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
1299
4890
  .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1300
4891
  .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
1301
4892
  .dss_footerIcon{flex:none;font-size:15px;line-height:1}
1302
4893
  .dss_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1303
4894
  .dss_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
1304
- .dss_panel{position:fixed;z-index:1001;left:50%;top:8vh;transform:translateX(-50%);width:min(760px,92vw);max-height:78vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
4895
+ .dss_panel{position:fixed;z-index:1001;left:50%;top:6vh;transform:translateX(-50%);width:min(860px,94vw);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
1305
4896
  .dss_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
1306
4897
  .dss_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
1307
4898
  .dss_tabRow{display:inline-flex;gap:4px}
@@ -1390,23 +4981,47 @@ window.__ModuleLoader__.load({
1390
4981
  function apply(ctx) {
1391
4982
  ctx.effect(() => injectStyles(), "dsh-manage-session: stylesheet");
1392
4983
  const locale = ctx.get("locale");
1393
- if (locale !== void 0 && typeof locale.register === "function") ctx.effect(() => locale.register(NS, {
1394
- zh,
1395
- en
1396
- }), "dsh-manage-session: dictionaries");
4984
+ if (locale !== void 0 && typeof locale.register === "function") {
4985
+ ctx.effect(() => locale.register(NS, dictionaries$1), "dsh-manage-session: dictionaries");
4986
+ ctx.effect(() => locale.register(NS$1, dictionaries), "dsh-manage-session: search dictionaries");
4987
+ }
4988
+ ctx.effect(() => injectStyles$1(), "dsh-manage-session: search stylesheet");
1397
4989
  const configForms = ctx.get("configForms");
1398
- const legacySettings = ctx.get("settingsScope");
1399
- const bound = configForms?.get("dsh-manage-session") ?? legacySettings?.bind({ namespace: "session-steward" });
4990
+ const bound = withBridgeFallback(configForms?.get(STEWARD_ENTRY_ID), stewardBridgeScope());
4991
+ const searchBound = withBridgeFallback(configForms?.get(STEWARD_ENTRY_ID), searchBridgeScope());
1400
4992
  const slots = ctx.get("slots");
1401
4993
  if (slots === void 0) return;
4994
+ const openSearchHit = (sessionId) => {
4995
+ openSessionThrough((name) => ctx.get(name), sessionId);
4996
+ };
4997
+ slots.inject("sidebar.footer.action", () => slots.register({
4998
+ name: "sidebar.footer.action",
4999
+ id: "dsh-search-index",
5000
+ order: 5
5001
+ }, (props) => (0, react.createElement)(SearchEntry, {
5002
+ ...props,
5003
+ open: openSearchHit,
5004
+ scope: searchBound
5005
+ })), "dsh-manage-session: sidebar footer search entry");
1402
5006
  slots.inject("sidebar.footer.action", () => slots.register({
1403
5007
  name: "sidebar.footer.action",
1404
5008
  id: STEWARD_ENTRY_ID,
1405
5009
  order: 6
1406
5010
  }, (props) => (0, react.createElement)(StewardEntry, {
1407
5011
  ...props,
1408
- scope: bound
5012
+ scope: bound,
5013
+ openSession: openSearchHit
1409
5014
  })), "dsh-manage-session: sidebar footer entry");
5015
+ slots.inject("dsh-family.tab", () => slots.register({
5016
+ name: "dsh-family.tab",
5017
+ id: "dsh-search-index",
5018
+ order: 30,
5019
+ label: () => translate$1(locale?.bind?.(NS$1), "card.title"),
5020
+ locale: NS$1
5021
+ }, (props) => (0, react.createElement)(SearchSettingsCard, {
5022
+ scope: searchBound,
5023
+ t: props.t
5024
+ })), "dsh-manage-session: family settings tab (search)");
1410
5025
  slots.inject("dsh-family.tab", () => slots.register({
1411
5026
  name: "dsh-family.tab",
1412
5027
  id: STEWARD_ENTRY_ID,
@@ -1417,6 +5032,19 @@ window.__ModuleLoader__.load({
1417
5032
  scope: bound,
1418
5033
  t: props.t
1419
5034
  })), "dsh-manage-session: family settings tab");
5035
+ slots.inject("plugins.bundle.config", () => slots.register({
5036
+ name: "plugins.bundle.config",
5037
+ key: STEWARD_ENTRY_ID
5038
+ }, (props) => (0, react.createElement)("div", { style: {
5039
+ display: "grid",
5040
+ gap: "12px"
5041
+ } }, [(0, react.createElement)(SearchSettingsCard, {
5042
+ scope: searchBound,
5043
+ t: props.t
5044
+ }), (0, react.createElement)(StewardSettingsCard, {
5045
+ scope: bound,
5046
+ t: props.t
5047
+ })])), "dsh-manage-session: plugins page config card");
1420
5048
  const familyTabsHooks = makeFamilyTabsHooks(slots);
1421
5049
  ctx.effect(() => {
1422
5050
  let claimed;
@@ -1447,8 +5075,8 @@ window.__ModuleLoader__.load({
1447
5075
  const FAMILY_SECTION_ID = "dsh-family";
1448
5076
  /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
1449
5077
  const FAMILY_CHILD_KEY = "dsh-family.tab";
1450
- /** 接管宽限期:guard 2000ms 先试,本插件 2600ms 兜底。 */
1451
- const FAMILY_HOST_GRACE_MS = 2600;
5078
+ /** 接管宽限期(由分级派生)。 */
5079
+ const FAMILY_HOST_GRACE_MS = 2900;
1452
5080
  /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形;locale 变更也触发重渲染)。 */
1453
5081
  function makeFamilyTabsHooks(slots) {
1454
5082
  let version = -1;
@@ -1500,7 +5128,8 @@ window.__ModuleLoader__.load({
1500
5128
  };
1501
5129
  const historyFiles = config.historyFiles !== false;
1502
5130
  const healthCheck = config.healthCheck !== false;
1503
- const visible = config.enabled !== false && (historyFiles || healthCheck);
5131
+ const searchEnabled = config.search !== false;
5132
+ const visible = config.enabled !== false && (historyFiles || healthCheck || searchEnabled);
1504
5133
  const wide = props.wide === true;
1505
5134
  const label = translate(void 0, "panel.title");
1506
5135
  if (!visible) return (0, react.createElement)("span", { className: "dss_entryWrap" });
@@ -1516,11 +5145,28 @@ window.__ModuleLoader__.load({
1516
5145
  key: "panel",
1517
5146
  historyFiles,
1518
5147
  healthCheck,
5148
+ searchEnabled,
5149
+ openSession: props.openSession,
1519
5150
  onClose: () => {
1520
5151
  setOpen(false);
1521
5152
  }
1522
5153
  }) : null]);
1523
5154
  }
5155
+ /**
5156
+ * 搜索侧栏入口(合并包装配):双门可见性 —— 插件总开关 `enabled` 与搜索子域
5157
+ * 开关 `search` 任一关闭即不渲染;可见时交给 SwitchFooter(其内部再读一次
5158
+ * scope 的 enabled,与设置卡「启用会话搜索」同源,开关不会空转)。
5159
+ */
5160
+ function SearchEntry(props) {
5161
+ const scope = props.scope;
5162
+ const value = (0, react.useSyncExternalStore)((listener) => scope?.subscribe(listener) ?? (() => {}), () => scope?.getSnapshot())?.value ?? {};
5163
+ if (value["enabled"] === false || value["search"] === false) return (0, react.createElement)("span", { className: "dss_entryWrap" });
5164
+ return (0, react.createElement)("span", { className: props.wide === true ? "dss_entryWrap dss_entryWrapWide" : "dss_entryWrap" }, (0, react.createElement)(SwitchFooter, {
5165
+ wide: props.wide === true,
5166
+ open: props.open,
5167
+ scope: props.scope
5168
+ }));
5169
+ }
1524
5170
  //#endregion
1525
5171
  exports.NS = NS;
1526
5172
  exports.STEWARD_ENTRY_ID = STEWARD_ENTRY_ID;