dsh-config-manager 0.1.78 → 0.1.79

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 (290) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +400 -112
  26. package/lib/client.js +2845 -1524
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/autosync-config.d.ts +73 -0
  82. package/lib/sync/autosync-config.js +89 -0
  83. package/lib/sync/autosync-scheduler.d.ts +80 -6
  84. package/lib/sync/autosync-scheduler.js +263 -40
  85. package/lib/sync/gist/gist-transport.d.ts +15 -1
  86. package/lib/sync/gist/gist-transport.js +66 -5
  87. package/lib/sync/gist/messages.d.ts +3 -0
  88. package/lib/sync/gist/messages.js +7 -0
  89. package/lib/sync/git/git-transport.d.ts +42 -3
  90. package/lib/sync/git/git-transport.js +146 -3
  91. package/lib/sync/merge.d.ts +47 -14
  92. package/lib/sync/merge.js +242 -222
  93. package/lib/sync/messages.d.ts +8 -0
  94. package/lib/sync/messages.js +19 -0
  95. package/lib/sync/risk.d.ts +19 -2
  96. package/lib/sync/risk.js +10 -3
  97. package/lib/sync/s3/messages.d.ts +6 -0
  98. package/lib/sync/s3/messages.js +13 -0
  99. package/lib/sync/s3/s3-transport.d.ts +9 -1
  100. package/lib/sync/s3/s3-transport.js +78 -6
  101. package/lib/sync/sync-engine.d.ts +193 -4
  102. package/lib/sync/sync-engine.js +450 -37
  103. package/lib/sync/sync-history.d.ts +11 -0
  104. package/lib/sync/sync-history.js +20 -0
  105. package/lib/sync/sync-state.d.ts +43 -0
  106. package/lib/sync/sync-state.js +44 -0
  107. package/lib/sync/sync-watcher.d.ts +76 -0
  108. package/lib/sync/sync-watcher.js +202 -0
  109. package/lib/sync/text-merge.d.ts +85 -0
  110. package/lib/sync/text-merge.js +513 -0
  111. package/lib/sync/transport.d.ts +48 -0
  112. package/lib/sync/transport.js +12 -0
  113. package/lib/sync/watch-scope.d.ts +38 -0
  114. package/lib/sync/watch-scope.js +95 -0
  115. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  116. package/lib/sync/webdav/webdav-transport.js +128 -11
  117. package/lib/ui/foreign-view.js +10 -0
  118. package/lib/ui/i18n.d.ts +8 -0
  119. package/lib/ui/i18n.js +20 -0
  120. package/lib/ui/rollback-view.d.ts +12 -0
  121. package/lib/ui/rollback-view.js +2 -0
  122. package/lib/ui/sync-settings-view.d.ts +47 -0
  123. package/lib/ui/sync-settings-view.js +24 -0
  124. package/lib/ui/types.d.ts +21 -0
  125. package/lib/utils/env-lock.d.ts +1 -0
  126. package/lib/utils/env-lock.js +7 -4
  127. package/package.json +6 -6
  128. package/src/adapters/credentials.ts +22 -4
  129. package/src/adapters/file-collection.ts +2 -1
  130. package/src/adapters/mcp.test.ts +235 -1
  131. package/src/adapters/mcp.ts +276 -5
  132. package/src/adapters/messages.ts +7 -0
  133. package/src/adapters/plugin-files.ts +2 -1
  134. package/src/adapters/plugins.test.ts +176 -2
  135. package/src/adapters/plugins.ts +107 -21
  136. package/src/adapters/pnpm-workspace.test.ts +89 -1
  137. package/src/adapters/pnpm-workspace.ts +127 -2
  138. package/src/adapters/prompts.ts +20 -3
  139. package/src/adapters/providers.ts +62 -4
  140. package/src/adapters/settings.ts +16 -3
  141. package/src/adapters/skills.test.ts +141 -0
  142. package/src/adapters/skills.ts +58 -2
  143. package/src/adapters/ui.ts +16 -3
  144. package/src/adapters/units-entries.test.ts +190 -0
  145. package/src/adapters/units.ts +50 -0
  146. package/src/adapters/workspaces.ts +2 -1
  147. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  148. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  149. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  150. package/src/client/common/Skeleton.tsx +32 -0
  151. package/src/client/common/routes.ts +9 -0
  152. package/src/client/config-manager.module.css +321 -11
  153. package/src/client/home/HomePanel.tsx +14 -6
  154. package/src/client/import/ForeignImportView.tsx +3 -1
  155. package/src/client/import/import-wizard-steps.tsx +2 -1
  156. package/src/client/library/ArtifactRow.tsx +12 -2
  157. package/src/client/library/LibraryActions.tsx +6 -3
  158. package/src/client/library/LibraryPanel.tsx +21 -3
  159. package/src/client/library/RestorePlanView.tsx +4 -2
  160. package/src/client/locales.ts +3 -0
  161. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  162. package/src/client/recovery/RollbackCard.tsx +7 -2
  163. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  164. package/src/client/recovery/recovery-locales.ts +20 -2
  165. package/src/client/recovery/recovery-view.test.ts +39 -1
  166. package/src/client/recovery/recovery-view.ts +23 -1
  167. package/src/client/run-store.test.ts +1 -1
  168. package/src/client/run-store.ts +4 -0
  169. package/src/client/sync/AutosyncCard.tsx +49 -6
  170. package/src/client/sync/MergeModal.tsx +219 -0
  171. package/src/client/sync/MergePreviewView.tsx +306 -0
  172. package/src/client/sync/SyncActivityBar.tsx +31 -0
  173. package/src/client/sync/SyncConfirmView.tsx +2 -1
  174. package/src/client/sync/SyncPanel.tsx +130 -87
  175. package/src/client/sync/SyncProbeRow.tsx +162 -0
  176. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  177. package/src/client/sync/WorkDirCard.tsx +44 -6
  178. package/src/client/sync/history-model.ts +4 -1
  179. package/src/client/sync/merge-view.test.ts +234 -0
  180. package/src/client/sync/merge-view.ts +371 -0
  181. package/src/client/sync/probe-view.test.ts +90 -0
  182. package/src/client/sync/probe-view.ts +63 -0
  183. package/src/client/sync/remote-device-view.test.ts +35 -0
  184. package/src/client/sync/remote-device-view.ts +25 -0
  185. package/src/client/sync/sync-api.ts +212 -2
  186. package/src/client/sync/sync-entry.test.ts +78 -0
  187. package/src/client/sync/sync-entry.ts +53 -0
  188. package/src/client/sync/sync-locales.ts +234 -2
  189. package/src/client/sync/sync-view.test.ts +18 -1
  190. package/src/client/sync/sync-view.ts +20 -0
  191. package/src/core/backup.test.ts +41 -0
  192. package/src/core/backup.ts +20 -0
  193. package/src/core/mcp-connector-store.test.ts +265 -0
  194. package/src/core/mcp-connector-store.ts +516 -0
  195. package/src/core/messages-core.ts +4 -0
  196. package/src/core/messages.ts +46 -0
  197. package/src/core/phase3-host.ts +18 -1
  198. package/src/core/phase3-production-integration.test.ts +4 -0
  199. package/src/core/plugin-cli.test.ts +26 -0
  200. package/src/core/plugin-cli.ts +30 -4
  201. package/src/core/recovery-orchestrator.test.ts +104 -5
  202. package/src/core/recovery-orchestrator.ts +74 -17
  203. package/src/core/rollback-activity.test.ts +311 -0
  204. package/src/core/rollback-activity.ts +367 -0
  205. package/src/core/rollback-cross-session.test.ts +694 -0
  206. package/src/core/rollback-interrupted.test.ts +137 -0
  207. package/src/core/rollback-ledger.ts +32 -0
  208. package/src/core/rollback-merge.test.ts +130 -0
  209. package/src/core/rollback-merge.ts +78 -14
  210. package/src/core/rollback-point.ts +468 -91
  211. package/src/core/rollback-undo-journal.test.ts +508 -0
  212. package/src/core/rollback-undo-journal.ts +571 -0
  213. package/src/core/self-update.ts +2 -5
  214. package/src/core/text-fidelity.test.ts +189 -0
  215. package/src/core/text-fidelity.ts +197 -0
  216. package/src/core/types.ts +11 -3
  217. package/src/core/update-check.ts +2 -5
  218. package/src/core/validator.ts +14 -0
  219. package/src/foreign/bundle-origin.test.ts +265 -0
  220. package/src/foreign/bundle.ts +45 -5
  221. package/src/foreign/claude-code.test.ts +268 -0
  222. package/src/foreign/claude-code.ts +406 -25
  223. package/src/foreign/claude-plugins.test.ts +234 -0
  224. package/src/foreign/claude-sessions.test.ts +141 -0
  225. package/src/foreign/claude-sessions.ts +179 -23
  226. package/src/foreign/file-budget.test.ts +4 -1
  227. package/src/foreign/read-claude-code.test.ts +320 -0
  228. package/src/foreign/read-claude-code.ts +490 -71
  229. package/src/foreign/read-claude-plugins.ts +465 -0
  230. package/src/foreign/registry.ts +25 -2
  231. package/src/foreign/session-ir.test.ts +65 -0
  232. package/src/foreign/truth-table.test.ts +81 -0
  233. package/src/foreign/truth-table.ts +6 -3
  234. package/src/foreign/types.ts +162 -2
  235. package/src/index.facade.test.ts +26 -4
  236. package/src/index.ts +76 -19
  237. package/src/market/git-file-writer.test.ts +4 -0
  238. package/src/market/git-file-writer.ts +7 -1
  239. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  240. package/src/profiles/dsh-profile-launcher.ts +56 -1
  241. package/src/profiles/dsh-profile-shared.ts +8 -2
  242. package/src/routes/foreign.ts +8 -1
  243. package/src/routes/index.ts +1 -1
  244. package/src/routes/kit.ts +2 -2
  245. package/src/routes/sync.ts +176 -9
  246. package/src/schema/manifest-schema.test.ts +87 -2
  247. package/src/schema/manifest.ts +57 -1
  248. package/src/schema/types.ts +45 -2
  249. package/src/sync/autosync-config.test.ts +73 -1
  250. package/src/sync/autosync-config.ts +124 -0
  251. package/src/sync/autosync-risk-tier.test.ts +38 -2
  252. package/src/sync/autosync-scheduler.test.ts +264 -12
  253. package/src/sync/autosync-scheduler.ts +296 -44
  254. package/src/sync/gist/gist-conditional-write.test.ts +223 -0
  255. package/src/sync/gist/gist-transport.ts +73 -7
  256. package/src/sync/gist/messages.ts +7 -0
  257. package/src/sync/git/git-probe.test.ts +80 -0
  258. package/src/sync/git/git-transport.test.ts +185 -0
  259. package/src/sync/git/git-transport.ts +178 -5
  260. package/src/sync/merge.test.ts +188 -55
  261. package/src/sync/merge.ts +282 -224
  262. package/src/sync/messages.ts +19 -0
  263. package/src/sync/risk.ts +29 -5
  264. package/src/sync/s3/messages.ts +13 -0
  265. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  266. package/src/sync/s3/s3-transport.ts +87 -9
  267. package/src/sync/sync-device-identity.test.ts +267 -0
  268. package/src/sync/sync-engine.test.ts +379 -0
  269. package/src/sync/sync-engine.ts +535 -36
  270. package/src/sync/sync-history.test.ts +16 -1
  271. package/src/sync/sync-history.ts +26 -0
  272. package/src/sync/sync-state.test.ts +64 -0
  273. package/src/sync/sync-state.ts +79 -0
  274. package/src/sync/sync-watcher.test.ts +197 -0
  275. package/src/sync/sync-watcher.ts +246 -0
  276. package/src/sync/text-merge.test.ts +358 -0
  277. package/src/sync/text-merge.ts +554 -0
  278. package/src/sync/transport.ts +57 -0
  279. package/src/sync/watch-scope.test.ts +122 -0
  280. package/src/sync/watch-scope.ts +120 -0
  281. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  282. package/src/sync/webdav/webdav-transport.ts +137 -14
  283. package/src/ui/foreign-view.test.ts +34 -0
  284. package/src/ui/foreign-view.ts +10 -0
  285. package/src/ui/i18n.ts +20 -0
  286. package/src/ui/rollback-view.ts +14 -0
  287. package/src/ui/sync-settings-view.test.ts +28 -1
  288. package/src/ui/sync-settings-view.ts +43 -0
  289. package/src/ui/types.ts +22 -0
  290. package/src/utils/env-lock.ts +8 -5
@@ -25,7 +25,7 @@ import type { TranslateNS } from '../client-types.ts'
25
25
  import { Badge, Banner, Button, Card, Empty, FoldToggle, IconButton, SectionTitle, Spinner, StatusDot } from '../common/ui.tsx'
26
26
  import { DiskUsageCard } from '../environment/maintenance/DiskUsageCard.tsx'
27
27
  import { RollbackCard } from './RollbackCard.tsx'
28
- import { SkeletonList } from '../common/Skeleton.tsx'
28
+ import { SkeletonList, SkeletonTable } from '../common/Skeleton.tsx'
29
29
  import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
30
30
  import { Modal } from '../common/Modal.tsx'
31
31
  import { Icon } from '../common/Icon.tsx'
@@ -76,7 +76,7 @@ import { runStore, type RecoveryStoreSlice } from '../run-store.ts'
76
76
  import type { CrashReport, IncidentApi, RescueStatus } from './incident-api.ts'
77
77
  import { IncompleteCopiesSection } from './IncompleteCopiesSection.tsx'
78
78
  import {
79
- isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, toRecoveryPreviewView,
79
+ isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, recoveryBlockReason, toRecoveryPreviewView,
80
80
  toRecoveryView,
81
81
  crashAdviceKey,
82
82
  crashReasonKey,
@@ -874,7 +874,8 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
874
874
  {phase === 'error' && <Banner kind="error">{error ?? t('common.unknownError')}</Banner>}
875
875
 
876
876
  {/* 首次扫描(还没有任何结果):给出进行中反馈 —— 旧实现在这里只留一片空白 */}
877
- {phase === 'loading' && response === null && <Spinner label={t('sessions.scanning')} />}
877
+ {/* 首次扫描:按会话表的形状铺骨架(表格行),扫描结果到货时表格原地长出来 */}
878
+ {phase === 'loading' && response === null && <SkeletonTable rows={6} label={t('sessions.scanning')} />}
878
879
 
879
880
  {phase === 'ready' && response !== null && (
880
881
  <>
@@ -1486,6 +1487,14 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
1486
1487
  const previewView = state.preview !== null ? toRecoveryPreviewView(state.preview) : null
1487
1488
  /** 真有待处理事项(口径唯一来源 = 纯函数 recovery-view.ts 的 recoveryRequired):决定副标题与空态 */
1488
1489
  const needsAttention = view?.recoveryRequired === true
1490
+ /**
1491
+ * 选中项是否属于「正在正常执行中」的操作。是 ⇒ 恢复/放弃都不该点:它还在跑,
1492
+ * quarantine 会把在跑的 journal 从 active/ 抽走。据此给出**原因**而不是灰按钮。
1493
+ */
1494
+ const selectedLive = selected !== undefined
1495
+ && (view?.activeOperations.some((op) => op.operationId === selected.operationId) ?? false)
1496
+ /** 选中项「执行恢复」不可用的原因(null = 可用)——见 recovery-view.ts recoveryBlockReason */
1497
+ const selectedBlockReason = selected !== undefined ? recoveryBlockReason(selected) : null
1489
1498
 
1490
1499
  return (
1491
1500
  <div className={css.viewBody}>
@@ -1514,9 +1523,15 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
1514
1523
  )}
1515
1524
 
1516
1525
  {/* —— 状态与待处理事项(先异常、后诊断):三块诊断卡在下方,缺省只展开磁盘占用 —— */}
1517
- {/* SAFE MODE / recovery-required 状态提示(正常态不渲染任何横幅——无事项即静默) */}
1526
+ {/* SAFE MODE / recovery-required 状态提示(正常态不渲染任何横幅——无事项即静默)。
1527
+ **文案必须与真实阻断态同源**:recoveryRequired 只是「有待处理事项」,不等于写操作已被
1528
+ 暂停(残留锁 / 中断的档案复制都不会让 safeMode.blocked 为 true)。此前这里无条件说
1529
+ 「已进入安全模式(会修改配置的操作已暂停)」⇒ 用户看到保护提示,实际上许多写操作照常
1530
+ 成功,属于假警报(也让真正被阻断的场景失去可信度)。 */}
1518
1531
  {view?.recoveryRequired === true && (
1519
- <Banner kind="error">{t('recovery.currentState.safeMode')}</Banner>
1532
+ <Banner kind="error">
1533
+ {view.safeModeBlocked ? t('recovery.currentState.safeMode') : t('recovery.currentState.itemsOnly')}
1534
+ </Banner>
1520
1535
  )}
1521
1536
 
1522
1537
  {state.status === 'loading' && <SkeletonList label={t('recovery.loading')} />}
@@ -1564,6 +1579,26 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
1564
1579
  </Card>
1565
1580
  )}
1566
1581
 
1582
+ {/* 正在正常执行的操作(不是 incident):给一句「会自行结束」,避免用户以为面板出了故障。
1583
+ 这一块存在的理由:此前 autosync push 在跑的 12 秒里,它被当成了待处理事故。 */}
1584
+ {state.status === 'ready' && (view?.activeOperations.length ?? 0) > 0 && (
1585
+ <Card className={css.card}>
1586
+ <div className={css.groupLabel}>{t('recovery.live.title')}</div>
1587
+ <div className={css.hint}>{t('recovery.live.hint')}</div>
1588
+ <div className={css.snapshotList}>
1589
+ {view!.activeOperations.map((op) => (
1590
+ <div key={op.operationId} className={css.snapshotRow}>
1591
+ <span className={css.snapshotRowMain} title={op.operationId}>
1592
+ <span>{op.operationType}</span>
1593
+ <span>{new Date(op.startedAt).toLocaleString()}</span>
1594
+ <span><Badge kind="info">{incidentStateLabel(t, op.state)}</Badge></span>
1595
+ </span>
1596
+ </div>
1597
+ ))}
1598
+ </div>
1599
+ </Card>
1600
+ )}
1601
+
1567
1602
  {state.status === 'ready' && (view?.incidents.length ?? 0) > 0 && (
1568
1603
  <>
1569
1604
  {/* 进行中 recovery run 提示 */}
@@ -1691,13 +1726,25 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
1691
1726
 
1692
1727
  {state.actionError !== null && <Banner kind="error">{state.actionError}</Banner>}
1693
1728
 
1729
+ {/* 「执行恢复」不可用的原因:灰按钮不带解释 = 用户只能盲猜(这正是事故里的体验)。
1730
+ 三种原因互斥且都能被代码逐条证明:操作仍在跑 / 无绑定快照 / 决策为需人工处理。 */}
1731
+ {selectedLive ? (
1732
+ <Banner kind="info">{t('recovery.incident.liveBlocked')}</Banner>
1733
+ ) : state.verifyResult === null && selectedBlockReason !== null ? (
1734
+ <div className={css.hint}>
1735
+ {selectedBlockReason === 'noSnapshot'
1736
+ ? t('recovery.incident.blockReason.noSnapshot')
1737
+ : t('recovery.incident.blockReason.needsAttention')}
1738
+ </div>
1739
+ ) : null}
1740
+
1694
1741
  {/* 动作区 */}
1695
1742
  <div className={css.actionRow}>
1696
1743
  {state.verifyResult === null && (
1697
1744
  <>
1698
1745
  <Button
1699
1746
  variant="danger"
1700
- disabled={state.running || !(previewView?.actionable ?? false)}
1747
+ disabled={state.running || selectedLive || !(previewView?.actionable ?? false)}
1701
1748
  loading={state.running}
1702
1749
  onClick={() => { setConfirmOpen(true) }}
1703
1750
  >
@@ -1705,7 +1752,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
1705
1752
  </Button>
1706
1753
  <Button
1707
1754
  variant="ghost"
1708
- disabled={state.running}
1755
+ disabled={state.running || selectedLive}
1709
1756
  onClick={() => { setDismissOpen(true) }}
1710
1757
  >
1711
1758
  {t('recovery.dismiss')}
@@ -57,6 +57,7 @@ import type {
57
57
  } from '../../ui/rollback-view.ts'
58
58
  import { redact } from '../../security/redaction.ts'
59
59
  import { Badge, Banner, Button, Card, Checkbox, Empty, IconButton, SectionTitle, Spinner } from '../common/ui.tsx'
60
+ import { SkeletonList } from '../common/Skeleton.tsx'
60
61
  import { Icon } from '../common/Icon.tsx'
61
62
  import { InfoHint } from '../common/InfoHint.tsx'
62
63
  import { Modal } from '../common/Modal.tsx'
@@ -460,7 +461,7 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
460
461
  </>
461
462
  )}
462
463
 
463
- {status === 'loading' && <Spinner label={t('recovery.loading')} />}
464
+ {status === 'loading' && <SkeletonList count={3} label={t('recovery.loading')} />}
464
465
  {status === 'error' && (
465
466
  <div className={css.actionRow}>
466
467
  <Banner kind="error">{error !== null ? error : t('rollback.apiMissing')}</Banner>
@@ -544,7 +545,7 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
544
545
  )}
545
546
  />
546
547
  <Modal.Body scroll>
547
- {status === 'loading' && <Spinner label={t('recovery.loading')} />}
548
+ {status === 'loading' && <SkeletonList count={3} label={t('recovery.loading')} />}
548
549
  {status === 'error' && (
549
550
  <div className={css.actionRow}>
550
551
  <Banner kind="error">{error !== null ? error : t('rollback.apiMissing')}</Banner>
@@ -829,6 +830,10 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
829
830
  {segment.unavailableCount > 0 && (
830
831
  <span>{t('rollback.result.unavailable', { n: segment.unavailableCount })}</span>
831
832
  )}
833
+ {segment.unclaimedCount > 0 && (
834
+ <span>{t('rollback.result.unclaimed', { n: segment.unclaimedCount })}</span>
835
+ )}
836
+ {segment.unclaimedPaths.map((line) => <span key={'unclaimed:' + line}>{line}</span>)}
832
837
  {segment.concurrentCount > 0 && (
833
838
  <span>{t('rollback.result.concurrent', { n: segment.concurrentCount })}</span>
834
839
  )}
@@ -12,7 +12,8 @@ import type { TranslateNS } from '../client-types.ts'
12
12
  import type { RollbackFileDiffResult } from '../../ui/rollback-view.ts'
13
13
  import { rollbackFileDiffComparable, rollbackFileDiffReasonKey } from '../../ui/rollback-view.ts'
14
14
  import { redact } from '../../security/redaction.ts'
15
- import { Banner, Spinner } from '../common/ui.tsx'
15
+ import { Banner } from '../common/ui.tsx'
16
+ import { SkeletonTable } from '../common/Skeleton.tsx'
16
17
  import { DiffTable } from '../common/DiffTable.tsx'
17
18
  import css from '../config-manager.module.css'
18
19
 
@@ -48,7 +49,7 @@ export function RollbackDiffPane({ state, rt, title, fill }: RollbackDiffPanePro
48
49
  */
49
50
  const shell = (body: ReactNode): ReactNode =>
50
51
  fill === true ? <div className={css.diffPane + ' ' + css.diffPaneFill}>{body}</div> : body
51
- if (state.status === 'loading') return shell(<Spinner label={rt('rollback.diff.loading')} />)
52
+ if (state.status === 'loading') return shell(<SkeletonTable rows={8} label={rt('rollback.diff.loading')} />)
52
53
  if (state.status === 'error') return shell(<Banner kind="error">{state.message === null ? rt('rollback.diff.failed') : redact(state.message)}</Banner>)
53
54
  const diff = state.diff
54
55
  if (diff.ok !== true) {
@@ -23,7 +23,8 @@ export const zh = {
23
23
  'recovery.banner': '配置修改已被保护:存在未完成的恢复事项。请先处理后继续。',
24
24
  'recovery.bannerAction': '去处理',
25
25
  'recovery.required': '需要恢复',
26
- 'recovery.requiredHint': '检测到一次操作未正常完成,为安全起见已暂停所有会修改配置的操作。请处理以下恢复事项以恢复正常。',
26
+ // 「有待处理事项」≠「写操作已被暂停」:后者只有 safeMode.blocked 才成立(横幅文案另有一对键)。
27
+ 'recovery.requiredHint': '存在需要处理的恢复事项(残留锁、未完成的操作或中断的复制)。处理完这些事项即恢复正常。',
27
28
  'recovery.rollbackRecommended': '建议回滚',
28
29
  'recovery.rollbackContinue': '续跑回滚',
29
30
  'recovery.inProgress': '恢复进行中',
@@ -54,6 +55,15 @@ export const zh = {
54
55
  // 当前状态
55
56
  'recovery.currentState.title': '当前状态',
56
57
  'recovery.currentState.safeMode': '已进入安全模式(会修改配置的操作已暂停)',
58
+ // 有待处理事项、但 safeMode.blocked 为 false(残留锁 / 中断复制 / 旧宿主):不得声称写操作已被暂停
59
+ 'recovery.currentState.itemsOnly': '存在需要处理的恢复事项(写操作未被暂停)',
60
+ // 正在正常执行的操作(不是 incident)
61
+ 'recovery.live.title': '正在执行的操作',
62
+ 'recovery.live.hint': '这些操作正在正常执行中,不是未完成的恢复事项;结束后会自动消失。',
63
+ // 「执行恢复 / 放弃恢复」不可用的原因(灰按钮必须能自证)
64
+ 'recovery.incident.liveBlocked': '该操作仍在进行中,现在不能恢复也不能放弃,请等它结束。',
65
+ 'recovery.incident.blockReason.noSnapshot': '本次操作没有绑定可信恢复快照,因此无法执行恢复。',
66
+ 'recovery.incident.blockReason.needsAttention': '该事项被判定为需要人工处理,无法自动恢复。',
57
67
  'recovery.currentState.hasSnapshot': '存在可信恢复快照',
58
68
  'recovery.currentState.noSnapshot': '无可信恢复快照',
59
69
  // 快照信息
@@ -450,6 +460,7 @@ export const zh = {
450
460
  'rollback.result.conflict': '{n} 个文件两侧改到同一处,已保持原样 —— 需要你手工处理',
451
461
  'rollback.result.unavailable': '{n} 个文件无法安全撤销(缺少中间版本 / 二进制 / 差异过大),已保持原样',
452
462
  'rollback.result.concurrent': '{n} 个文件在回退过程中又被改过,已保持原样',
463
+ 'rollback.result.unclaimed': '{n} 个文件无法判定属于哪个对话(改动时没有任何对话在运行),已保持原样',
453
464
  'rollback.result.selectionSkipped': '{n} 个文件按你的勾选跳过了',
454
465
  'rollback.preview.files.empty': '工作区与该回退点一致,无需恢复',
455
466
  /* —— T15:**预览态**(点「回退」之前那一屏)的四态文案 ——
@@ -634,7 +645,7 @@ export const en: Record<keyof typeof zh, string> = {
634
645
  'recovery.banner': 'Configuration changes are protected: there is an unfinished recovery item. Resolve it before continuing.',
635
646
  'recovery.bannerAction': 'Go to recovery',
636
647
  'recovery.required': 'Recovery Required',
637
- 'recovery.requiredHint': 'An operation did not complete normally. To stay safe, all config-modifying operations are paused. Handle the recovery item(s) below to restore normal operation.',
648
+ 'recovery.requiredHint': 'Recovery item(s) need handling (a stale lock, an unfinished operation, or an interrupted copy). Resolve them to restore normal operation.',
638
649
  'recovery.rollbackRecommended': 'Rollback recommended',
639
650
  'recovery.rollbackContinue': 'Continue rollback',
640
651
  'recovery.inProgress': 'Recovery in progress',
@@ -661,6 +672,12 @@ export const en: Record<keyof typeof zh, string> = {
661
672
  'recovery.verify.verdict.unknown': 'Unknown',
662
673
  'recovery.currentState.title': 'Current state',
663
674
  'recovery.currentState.safeMode': 'Safe mode active (config-modifying operations paused)',
675
+ 'recovery.currentState.itemsOnly': 'Recovery item(s) need handling (write operations are not paused)',
676
+ 'recovery.live.title': 'Running operations',
677
+ 'recovery.live.hint': 'These operations are running normally; they are not unfinished recovery items and will disappear when they finish.',
678
+ 'recovery.incident.liveBlocked': 'This operation is still running, so it cannot be recovered or dismissed yet. Wait for it to finish.',
679
+ 'recovery.incident.blockReason.noSnapshot': 'This operation has no trusted recovery snapshot bound to it, so recovery cannot run.',
680
+ 'recovery.incident.blockReason.needsAttention': 'This item is classified as needing manual handling and cannot be recovered automatically.',
664
681
  'recovery.currentState.hasSnapshot': 'Trusted recovery snapshot available',
665
682
  'recovery.currentState.noSnapshot': 'No trusted recovery snapshot',
666
683
  'recovery.snapshot.title': 'Recovery snapshot',
@@ -1033,6 +1050,7 @@ export const en: Record<keyof typeof zh, string> = {
1033
1050
  'rollback.result.conflict': '{n} file(s) were changed on both sides in the same place and were left as they are — they need your decision',
1034
1051
  'rollback.result.unavailable': '{n} file(s) could not be reverted safely (no common ancestor / binary / diff too large) and were left as they are',
1035
1052
  'rollback.result.concurrent': '{n} file(s) changed again while this rewind was running and were left as they are',
1053
+ 'rollback.result.unclaimed': '{n} file(s) could not be attributed to any conversation (nothing was running when they changed) and were left as they are',
1036
1054
  'rollback.result.selectionSkipped': '{n} file(s) skipped by your selection',
1037
1055
  'rollback.preview.files.empty': 'The workspace already matches this rollback point, nothing to restore',
1038
1056
  'rollback.preview.files.unavailable': 'The workspace state of this rollback point has no usable record (reason: {reason}), nothing to restore',
@@ -9,7 +9,7 @@ import test from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
 
11
11
  import {
12
- isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, sortIncidentsByNewest,
12
+ isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, recoveryBlockReason, sortIncidentsByNewest,
13
13
  toRecoveryPreviewView, toRecoveryView, verdictToUiState,
14
14
  } from './recovery-view.ts';
15
15
  import type { RecoveryStatus } from '../../ui/types.ts';
@@ -220,3 +220,41 @@ test('sortIncidentsByNewest:按 createdAt 倒序', () => {
220
220
  assert.equal(sorted[0]!.operationId, 'b');
221
221
  assert.equal(sorted[1]!.operationId, 'a');
222
222
  });
223
+
224
+ // ---------- 正在正常执行的操作(本轮修复:不再当成「未完成的恢复事项」) ----------
225
+
226
+ test('toRecoveryView:live[] 映射为 activeOperations,且不改变 state / recoveryRequired', () => {
227
+ const v = toRecoveryView(mkStatus({
228
+ live: [{
229
+ operationId: '00000000-0000-4000-8000-0000000000bb',
230
+ operationType: 'autosync-push',
231
+ state: 'APPLYING',
232
+ startedAt: '2026-01-01T00:00:00.000Z',
233
+ }],
234
+ }));
235
+ assert.equal(v.activeOperations.length, 1);
236
+ assert.equal(v.activeOperations[0]!.operationType, 'autosync-push');
237
+ assert.equal(v.activeOperations[0]!.state, 'APPLYING');
238
+ // 正在跑不是问题:既不进 incidents,也不触发「需要恢复」的横幅
239
+ assert.deepEqual(v.incidents, []);
240
+ assert.equal(v.recoveryRequired, false);
241
+ assert.equal(v.state, 'NORMAL');
242
+ });
243
+
244
+ test('toRecoveryView:旧宿主不返回 live 字段 → activeOperations 为空数组(不误报正在跑)', () => {
245
+ const v = toRecoveryView({ incidents: [], running: [] });
246
+ assert.deepEqual(v.activeOperations, []);
247
+ });
248
+
249
+ test('recoveryBlockReason:与 actionable 严格互补(面板据此解释「为什么灰」)', () => {
250
+ // needs-attention 优先:即使有快照,阻塞原因是「需人工处理」而不是「没快照」
251
+ assert.equal(recoveryBlockReason({ decision: 'needs-attention', snapshotId: 'snap-1' }), 'needsAttention');
252
+ assert.equal(recoveryBlockReason({ decision: 'needs-attention', snapshotId: null }), 'needsAttention');
253
+ // 能恢复的决策但没有可信快照 → 无快照可回滚
254
+ assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: null }), 'noSnapshot');
255
+ // 判据必须与 actionable 逐字同口径(:108 只认 null / ''),所以这里也只断言这两种形态
256
+ assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: '' }), 'noSnapshot');
257
+ // 可执行 → 无阻塞原因
258
+ assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: 'snap-1' }), null);
259
+ assert.equal(recoveryBlockReason({ decision: 'rollback-continue', snapshotId: 'snap-1' }), null);
260
+ });
@@ -20,7 +20,7 @@
20
20
  * NEEDS_ATTENTION 显示为 recovered、绝不在 snapshot 不可信时显示可恢复。
21
21
  */
22
22
  import type {
23
- RecoveryDecision, RecoveryIncident, RecoveryPreview, RecoveryStatus, RecoveryVerdict,
23
+ RecoveryActiveOperation, RecoveryDecision, RecoveryIncident, RecoveryPreview, RecoveryStatus, RecoveryVerdict,
24
24
  } from '../../ui/types.ts';
25
25
 
26
26
  /** UI 顶层状态分类。 */
@@ -71,6 +71,12 @@ export interface RecoveryView {
71
71
  incidents: RecoveryIncidentView[];
72
72
  /** 进行中的 recovery run(runId + status)。 */
73
73
  running: { runId: string; status: string }[];
74
+ /**
75
+ * 正在正常执行的操作(不是 incident)。决定两件事:
76
+ * ① 面板要给出一句「这些在执行中,会自行结束」,而不是让用户对着灰按钮猜;
77
+ * ② 若某条 incident 恰好也在跑(宿主判定放宽/旧宿主),对应行的「执行恢复/放弃恢复」要解释原因。
78
+ */
79
+ activeOperations: RecoveryActiveOperation[];
74
80
  /** 需用户显式处理的残留锁(attention=true 时才非 null)。 */
75
81
  lock: RecoveryLockView | null;
76
82
  /**
@@ -128,6 +134,8 @@ export function toRecoveryView(status: RecoveryStatus): RecoveryView {
128
134
  recoveryRequired,
129
135
  incidents,
130
136
  running: status.running,
137
+ // 旧宿主不返回 live → 空数组(绝不误报「有操作在跑」)
138
+ activeOperations: status.live ?? [],
131
139
  lock,
132
140
  safeModeStuck,
133
141
  safeModeBlocked,
@@ -136,6 +144,20 @@ export function toRecoveryView(status: RecoveryStatus): RecoveryView {
136
144
  };
137
145
  }
138
146
 
147
+ /**
148
+ * 「执行恢复」为什么不可用(面板显示原因,而不是丢一个没有任何解释的灰按钮)。
149
+ *
150
+ * 判据与 `actionable` 严格互补(同一条规则两处呈现):decision=needs-attention 断了
151
+ * 恢复路径,snapshotId 为空断了快照来源。返回 null = 可执行。
152
+ */
153
+ export function recoveryBlockReason(
154
+ incident: Pick<RecoveryIncidentView, 'decision' | 'snapshotId'>,
155
+ ): 'needsAttention' | 'noSnapshot' | null {
156
+ if (incident.decision === 'needs-attention') return 'needsAttention';
157
+ if (incident.snapshotId === null || incident.snapshotId === '') return 'noSnapshot';
158
+ return null;
159
+ }
160
+
139
161
  /** 单个 preview 的渲染模型。 */
140
162
  export interface RecoveryPreviewView {
141
163
  operationId: string;
@@ -1617,7 +1617,7 @@ test('P0-9: 持久化字段清单显式化(键集合断言 —— 新增字段
1617
1617
  'loadError', 'pullReport', 'pushPreview', 'pushReport', 'repoUrl', 'webdavUrl', 'webdavUsername',
1618
1618
  ])
1619
1619
  assert.deepEqual(keys(p.sync.byChannel.git), [
1620
- 'autosync', 'autosyncEnabled', 'autosyncInterval', 'encrypt', 'includeSecrets',
1620
+ 'autosync', 'autosyncEnabled', 'autosyncInterval', 'autosyncRealtime', 'encrypt', 'includeSecrets',
1621
1621
  'loadingSnapshots', 'selectedSnapshotId', 'sessionsInclude', 'sessionsLimit', 'snapshots',
1622
1622
  'syncMode', 'syncSections',
1623
1623
  ])
@@ -1023,6 +1023,8 @@ export function toPersistedState(state: StoreState): PersistedState {
1023
1023
  autosync: c.autosync,
1024
1024
  autosyncEnabled: c.autosyncEnabled,
1025
1025
  autosyncInterval: c.autosyncInterval,
1026
+ // 真·自动同步的本地监听开关(用户设置,非敏感;缺省 true)
1027
+ autosyncRealtime: c.autosyncRealtime,
1026
1028
  })
1027
1029
  return {
1028
1030
  v: 1,
@@ -1645,6 +1647,8 @@ export class RunStore {
1645
1647
  autosync: null,
1646
1648
  autosyncEnabled: false,
1647
1649
  autosyncInterval: '30m',
1650
+ // 旧版持久化载荷没有这个字段 → 缺省开启(真·自动同步是缺省行为)
1651
+ autosyncRealtime: true,
1648
1652
  }
1649
1653
  : undefined
1650
1654
  return {
@@ -11,11 +11,12 @@ import { Badge, Card } from '../common/ui.tsx'
11
11
  import { Select } from '../common/Select.tsx'
12
12
  import { InfoHint } from '../common/InfoHint.tsx'
13
13
  import {
14
- autosyncIntervalMs, autosyncSkipDetail, autosyncStatusText, computeAutosyncCountdown, formatIntervalDuration,
14
+ autosyncFailureMessage, autosyncIntervalMs, autosyncSkipDetail, autosyncStatusText, computeAutosyncCountdown,
15
+ formatIntervalDuration,
15
16
  } from './sync-view.ts'
16
17
  // 展示文本渲染前统一脱敏(与 ErrorBanner/ReportView 同规矩)
17
18
  import { redact } from '../../security/redaction.ts'
18
- import type { AutosyncInterval } from './sync-api.ts'
19
+ import type { AutosyncAutoApplyTier, AutosyncConflictPolicy, AutosyncInterval } from './sync-api.ts'
19
20
  import type { ChannelSyncState } from './sync-view.ts'
20
21
  import { autosyncIntervalKey } from '../../ui/sync-settings-view.ts'
21
22
  import type { SyncAutosyncIntervalKey } from '../../ui/sync-settings-view.ts'
@@ -33,14 +34,20 @@ function intervalLabel(iv: AutosyncInterval, t: TranslateNS<'config-manager-sync
33
34
  /* ------------------------------------------- 拆分出的 render 段(t42,同文件内子组件) */
34
35
 
35
36
  /** 自动同步设置(当前通道):总开关 + 间隔下拉 + 状态徽章(文案/倒计时在卡内派生)。 */
36
- export function AutosyncCard({ t, uiT, settings, busy, onToggleAutosync, onUpdateInterval }: {
37
+ export function AutosyncCard({ t, uiT, settings, busy, onToggleAutosync, onUpdateInterval, onUpdateOptions }: {
37
38
  t: TranslateNS<'config-manager-sync'>
38
39
  uiT: UiT
39
40
  settings: Pick<ChannelSyncState, 'autosync' | 'autosyncEnabled' | 'autosyncInterval'>
40
41
  busy: boolean
41
42
  onToggleAutosync: (enabled: boolean) => void
42
43
  onUpdateInterval: (interval: AutosyncInterval) => void
44
+ /** 真·自动同步的三个策略项(realtime / 自动应用等级 / 冲突策略);只传要改的字段 */
45
+ onUpdateOptions: (patch: { realtime?: boolean; autoApplyTier?: AutosyncAutoApplyTier; conflictPolicy?: AutosyncConflictPolicy }) => void
43
46
  }) {
47
+ // 三件套读自宿主状态(缺省值集中在这里,与宿主 DEFAULT_AUTOSYNC_* 的口径一致)
48
+ const realtime = settings.autosync?.realtime ?? true
49
+ const autoApplyTier: AutosyncAutoApplyTier = settings.autosync?.autoApplyTier ?? 'medium'
50
+ const conflictPolicy: AutosyncConflictPolicy = settings.autosync?.conflictPolicy ?? 'local'
44
51
  const autosyncText = settings.autosync !== null ? autosyncStatusText(settings.autosync, uiT) : t('autosync.statusNever')
45
52
  /** 距下次自动同步剩余 ms(null = 从未运行;0 = 已到期) */
46
53
  const autosyncCountdownMs = settings.autosync !== null && settings.autosync.elapsedMs >= 0
@@ -49,9 +56,10 @@ export function AutosyncCard({ t, uiT, settings, busy, onToggleAutosync, onUpdat
49
56
  // issue #79:本轮为什么没同步 / 失败了什么,必须看得见 —— 此前只写进 info 级日志
50
57
  // (默认日志级别 warn),界面恒无提示,用户只能去翻 sync-autosync.json。
51
58
  const skipDetail = settings.autosync !== null ? autosyncSkipDetail(settings.autosync, t) : null
52
- const lastMessage = settings.autosync?.lastRunMessage !== undefined && settings.autosync.lastRunMessage !== ''
53
- ? redact(settings.autosync.lastRunMessage)
54
- : null
59
+ // issue #105:只有本轮真的失败才渲染错误原文。此前只看「非空」,于是上一轮(甚至十天前)
60
+ // 的旧错误会被当成当前结论继续展示;判据现在收在 autosyncFailureMessage 一处。
61
+ const failureMessage = settings.autosync !== null ? autosyncFailureMessage(settings.autosync) : null
62
+ const lastMessage = failureMessage === null ? null : redact(failureMessage)
55
63
  return (
56
64
  <Card>
57
65
  <span className={css.groupLabel}>{t('autosync.title')} <InfoHint text={t('autosync.description')} label={t('common.infoHint')} /></span>
@@ -74,6 +82,41 @@ export function AutosyncCard({ t, uiT, settings, busy, onToggleAutosync, onUpdat
74
82
  options={AUTOSYNC_INTERVAL_OPTIONS.map((iv) => ({ value: iv, label: intervalLabel(iv, t) }))}
75
83
  />
76
84
  </label>
85
+ <label className={css.checkboxRow}>
86
+ <input
87
+ type="checkbox"
88
+ checked={realtime}
89
+ disabled={busy}
90
+ onChange={(e: ChangeEvent<HTMLInputElement>) => { onUpdateOptions({ realtime: e.target.checked }) }}
91
+ />
92
+ <span>{t('autosync.realtime')} <InfoHint text={t('autosync.realtimeHint')} label={t('common.infoHint')} /></span>
93
+ </label>
94
+ <label className={css.field}>
95
+ <span className={css.fieldLabel}>{t('autosync.autoApplyTier')} <InfoHint text={t('autosync.autoApplyTierHint')} label={t('common.infoHint')} /></span>
96
+ <Select
97
+ value={autoApplyTier}
98
+ disabled={busy}
99
+ ariaLabel={t('autosync.autoApplyTier')}
100
+ onChange={(next) => { onUpdateOptions({ autoApplyTier: next as AutosyncAutoApplyTier }) }}
101
+ options={[
102
+ { value: 'low', label: t('autosync.tierLow') },
103
+ { value: 'medium', label: t('autosync.tierMedium') },
104
+ ]}
105
+ />
106
+ </label>
107
+ <label className={css.field}>
108
+ <span className={css.fieldLabel}>{t('autosync.conflictPolicy')} <InfoHint text={t('autosync.conflictPolicyHint')} label={t('common.infoHint')} /></span>
109
+ <Select
110
+ value={conflictPolicy}
111
+ disabled={busy}
112
+ ariaLabel={t('autosync.conflictPolicy')}
113
+ onChange={(next) => { onUpdateOptions({ conflictPolicy: next as AutosyncConflictPolicy }) }}
114
+ options={[
115
+ { value: 'local', label: t('autosync.conflictLocal') },
116
+ { value: 'ask', label: t('autosync.conflictAsk') },
117
+ ]}
118
+ />
119
+ </label>
77
120
  <div className={css.statRow}>
78
121
  <Badge kind={settings.autosync?.lastRunStatus === 'failed' ? 'error' : settings.autosync?.lastRunStatus === 'skipped' ? 'warn' : 'info'}>
79
122
  {autosyncText}