dsh-desktop-kit 2.2.1 → 2.7.2

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 (125) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +113 -27
  3. package/client.js +3121 -32
  4. package/docs/AGENT-HARNESS.md +144 -0
  5. package/docs/ARCHITECTURE.md +38 -7
  6. package/docs/COMPATIBILITY.md +4 -2
  7. package/docs/DEVELOPMENT.md +56 -0
  8. package/docs/INSTALL.md +20 -11
  9. package/docs/LAPTOP-INSTALL.md +96 -0
  10. package/docs/ROADMAP-3.0.md +353 -0
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/SUBAGENTS.md +25 -0
  13. package/docs/TEST-0.2.2.md +5 -5
  14. package/docs/TEST-2.3.0.md +49 -0
  15. package/docs/TEST-2.7.2.md +46 -0
  16. package/docs/TOKENS.md +2 -2
  17. package/docs/TROUBLESHOOTING.md +27 -0
  18. package/docs/UI-QA.md +28 -0
  19. package/docs/images/peak-toast-peak.png +0 -0
  20. package/docs/images/peak-toast-soon.png +0 -0
  21. package/docs/preview/peak-toast.html +155 -0
  22. package/index.js +18 -1
  23. package/package.json +26 -6
  24. package/plugins/dsh-agent-extensions/README.md +99 -0
  25. package/plugins/dsh-agent-extensions/index.js +167 -0
  26. package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
  27. package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
  28. package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
  29. package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
  30. package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
  31. package/plugins/dsh-agent-extensions/lib/project.js +144 -0
  32. package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
  33. package/plugins/dsh-agent-extensions/package.json +9 -0
  34. package/plugins/dsh-browser-qa/README.md +221 -0
  35. package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
  36. package/plugins/dsh-browser-qa/index.js +165 -0
  37. package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
  38. package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
  39. package/plugins/dsh-browser-qa/lib/pages.js +552 -0
  40. package/plugins/dsh-browser-qa/lib/qa.js +827 -0
  41. package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
  42. package/plugins/dsh-browser-qa/package.json +31 -0
  43. package/plugins/dsh-checkpoints/README.md +22 -0
  44. package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
  45. package/plugins/dsh-checkpoints/index.js +80 -0
  46. package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
  47. package/plugins/dsh-checkpoints/package.json +25 -0
  48. package/plugins/dsh-github-review/README.md +131 -0
  49. package/plugins/dsh-github-review/cordis.patch.yml +3 -0
  50. package/plugins/dsh-github-review/index.js +536 -0
  51. package/plugins/dsh-github-review/lib/gh.js +494 -0
  52. package/plugins/dsh-github-review/lib/normalize.js +662 -0
  53. package/plugins/dsh-github-review/package.json +28 -0
  54. package/plugins/dsh-guardrails/README.md +264 -0
  55. package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
  56. package/plugins/dsh-guardrails/index.js +389 -0
  57. package/plugins/dsh-guardrails/lib/hooks.js +262 -0
  58. package/plugins/dsh-guardrails/lib/modes.js +77 -0
  59. package/plugins/dsh-guardrails/lib/policy.js +592 -0
  60. package/plugins/dsh-guardrails/lib/scan.js +451 -0
  61. package/plugins/dsh-guardrails/lib/secrets.js +274 -0
  62. package/plugins/dsh-guardrails/package.json +26 -0
  63. package/plugins/dsh-mcp-manager/README.md +78 -0
  64. package/plugins/dsh-mcp-manager/client.js +373 -0
  65. package/plugins/dsh-mcp-manager/index.js +159 -0
  66. package/plugins/dsh-mcp-manager/lib/api.js +250 -0
  67. package/plugins/dsh-mcp-manager/lib/client.js +219 -0
  68. package/plugins/dsh-mcp-manager/lib/config.js +217 -0
  69. package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
  70. package/plugins/dsh-mcp-manager/package.json +18 -0
  71. package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
  72. package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
  73. package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
  74. package/plugins/dsh-memory-bridge/README.md +161 -0
  75. package/plugins/dsh-memory-bridge/client.js +2264 -0
  76. package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
  77. package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
  78. package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
  79. package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
  80. package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
  81. package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
  82. package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
  83. package/plugins/dsh-memory-bridge/icon.svg +13 -0
  84. package/plugins/dsh-memory-bridge/index.js +378 -0
  85. package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
  86. package/plugins/dsh-memory-bridge/lib/github.js +473 -0
  87. package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
  88. package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
  89. package/plugins/dsh-memory-bridge/lib/state.js +565 -0
  90. package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
  91. package/plugins/dsh-memory-bridge/locale/en.json +6 -0
  92. package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
  93. package/plugins/dsh-memory-bridge/package.json +67 -0
  94. package/plugins/dsh-peak-guard/README.md +16 -3
  95. package/plugins/dsh-peak-guard/client.js +223 -8
  96. package/plugins/dsh-peak-guard/package.json +1 -1
  97. package/plugins/dsh-plan-mode/README.md +24 -0
  98. package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
  99. package/plugins/dsh-plan-mode/index.js +103 -0
  100. package/plugins/dsh-plan-mode/lib/plan.js +89 -0
  101. package/plugins/dsh-plan-mode/package.json +18 -0
  102. package/plugins/dsh-profile-transfer/client.js +277 -17
  103. package/plugins/dsh-profile-transfer/index.js +10 -0
  104. package/plugins/dsh-profile-transfer/package.json +3 -1
  105. package/plugins/dsh-profile-transfer/transfer.js +101 -2
  106. package/plugins/dsh-project-memory/index.js +92 -2
  107. package/plugins/dsh-project-memory/package.json +1 -1
  108. package/plugins/dsh-repo-map/README.md +26 -0
  109. package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
  110. package/plugins/dsh-repo-map/index.js +99 -0
  111. package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
  112. package/plugins/dsh-repo-map/package.json +25 -0
  113. package/plugins/dsh-skill-autopilot/index.js +135 -11
  114. package/skills/delivery-gate/SKILL.md +31 -0
  115. package/skills/git-and-ci/SKILL.md +5 -1
  116. package/skills/manifest.json +31 -6
  117. package/skills/requirements-and-planning/SKILL.md +3 -0
  118. package/skills/research-first/SKILL.md +35 -0
  119. package/skills/research-first/references/query-patterns.md +20 -0
  120. package/skills/subagent-playbook/SKILL.md +36 -0
  121. package/skills/subagent-playbook/references/prompts.md +39 -0
  122. package/skills/ui-craft/SKILL.md +43 -0
  123. package/skills/visual-qa/SKILL.md +29 -0
  124. package/skills/visual-qa/references/commands.md +32 -0
  125. package/skills-baseline.json +219 -0
@@ -0,0 +1,2264 @@
1
+ /**
2
+ * dsh-memory-bridge — browser half (v2, GitHub-only).
3
+ *
4
+ * Adds the “Память проектов” page to Settings. Every user connects their own
5
+ * GitHub account and their own private repository — the package ships no
6
+ * default owner/repo. Everything is done with buttons; the page never asks the
7
+ * user to work with slash commands.
8
+ */
9
+
10
+ window.__ModuleLoader__.load({
11
+ id: "dsh-memory-bridge",
12
+ factory: (require) => {
13
+ var module = { exports: {} };
14
+ var exports = module.exports;
15
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
16
+ const React = require("react");
17
+ const h = React.createElement;
18
+ const useEffect = React.useEffect;
19
+ const useRef = React.useRef;
20
+ const useState = React.useState;
21
+
22
+ const NS = "memory-bridge";
23
+ const RPC_NS = "dsh-memory-bridge";
24
+ const ALLOWED_INTERVALS = [60, 300, 600, 1800];
25
+ const REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u;
26
+
27
+ const EN = {
28
+ nav: "Project memory",
29
+ title: "Project memory",
30
+ subtitle: "Project-memory dossiers are synchronised through your own private GitHub repository. No commands are needed — every action is a button on this page.",
31
+ langProbe: "en",
32
+ loading: "Reading sync state…",
33
+ loadError: "Could not read the sync state",
34
+ retry: "Retry",
35
+ errorPrefix: "Could not complete:",
36
+ syncErrorLabel: "Sync error",
37
+ never: "not yet",
38
+ secRepo: "Repository",
39
+ secSync: "Synchronisation",
40
+ secHow: "How it works",
41
+ secSecurity: "Security",
42
+ secConflicts: "Conflicts",
43
+ secProjects: "Projects",
44
+ ghMissingTitle: "GitHub CLI is required",
45
+ ghMissingText: "The plugin works through GitHub CLI. Install it on this machine and sign in once.",
46
+ ghInstallStep: "Install GitHub CLI",
47
+ ghInstallHint: "Download and install GitHub CLI (gh) from cli.github.com.",
48
+ ghLoginStep: "Run in a terminal",
49
+ ghLoginHint: "Sign-in is needed once on each machine.",
50
+ checkAgain: "Check again",
51
+ checking: "Checking…",
52
+ ghLoginTitle: "Sign in to GitHub",
53
+ ghLoginText: "GitHub CLI is installed, but you are not signed in yet.",
54
+ repoSetupTitle: "Connect a repository",
55
+ repoSetupText: "Dossiers are synchronised through your own private GitHub repository.",
56
+ ghLoggedIn: "GitHub: signed in as",
57
+ accountHint: "The repository will be created under your account — your dossiers will not go to someone else's repository.",
58
+ repoLabel: "Repository",
59
+ repoPlaceholder: "owner/name",
60
+ createRepo: "Create private repository",
61
+ creatingRepo: "Creating…",
62
+ connectExisting: "Connect an existing private one",
63
+ connectingRepo: "Connecting…",
64
+ repoFieldHint: "owner/name, private only; the plugin checks visibility.",
65
+ repoInvalid: "Enter the repository as owner/name.",
66
+ privateOnlyNote: "The plugin refuses to work with a public repository.",
67
+ configureDone: "Repository connected",
68
+ createDone: "Private repository created and connected",
69
+ gitMissing: "Git was not found on this machine. Synchronisation cannot push changes without it.",
70
+ ghNotAuthorizedWarn: "GitHub CLI is not authorised — synchronisation cannot push changes.",
71
+ stateSynced: "Everything is synchronised",
72
+ stateLocalChanges: "There are local changes",
73
+ conflictOne: "conflict",
74
+ conflictFew: "conflicts",
75
+ conflictMany: "conflicts",
76
+ visPrivate: "Private",
77
+ visPublic: "PUBLIC — disable it!",
78
+ visUnknown: "Visibility not confirmed",
79
+ publicRepoWarn: "This repository is public. Disable synchronisation: project-memory dossiers must not be visible to outsiders.",
80
+ lastSync: "Last synchronisation",
81
+ branch: "Branch",
82
+ pendingLocal: "Local changes",
83
+ pendingUnpushed: "Unpushed changes",
84
+ enabledBadge: "Enabled",
85
+ disabledBadge: "Disabled",
86
+ syncNow: "Synchronise now",
87
+ syncing: "Synchronising…",
88
+ enable: "Enable",
89
+ disable: "Disable",
90
+ enabledDone: "Automatic sync enabled",
91
+ disabledDone: "Automatic sync disabled",
92
+ openRepo: "Open repository",
93
+ howButton: "How it works",
94
+ howShow: "Show",
95
+ howHide: "Collapse",
96
+ how1Title: "Private repository",
97
+ how1Text: "One private repository on your GitHub account keeps a shared copy of the project-memory dossiers.",
98
+ how2Title: "Three-way merge",
99
+ how2Text: "On every sync a machine compares “mine / previous common / from GitHub” and pushes the result back.",
100
+ how3Title: "Nothing is overwritten silently",
101
+ how3Text: "Additions from different workstations are merged. If the same line changed differently, a conflict appears and you choose the variant.",
102
+ sec1Title: "Private repository only",
103
+ sec1Text: "The plugin checks visibility and refuses to work with a public repository.",
104
+ sec2Title: "Secrets and PII are stripped",
105
+ sec2Text: "Sensitive values are removed and home paths are anonymised.",
106
+ sec3Title: "Conflicts never overwrite the file",
107
+ sec3Text: "Until you pick a variant, the local file stays untouched.",
108
+ sec4Title: "Backup and Git history",
109
+ sec4Text: "Every change keeps a backup and is visible in the Git history.",
110
+ optionsShow: "Options",
111
+ optionsHide: "Hide options",
112
+ optionsTitle: "Sync options",
113
+ autoSync: "Automatic sync",
114
+ autoSyncHint: "Synchronise on dossier changes and by the timer.",
115
+ intervalLabel: "Interval",
116
+ secondsShort: "s",
117
+ excludeLabel: "Excluded projects",
118
+ excludePlaceholder: "slug-one, slug-two",
119
+ excludeHint: "Project slugs separated by commas. Their dossiers are not synchronised.",
120
+ saveOptions: "Save",
121
+ saving: "Saving…",
122
+ optionsSaved: "Options saved",
123
+ syncDone: "Synchronisation finished",
124
+ summaryProjects: "Projects",
125
+ summaryFiles: "Files",
126
+ summaryConflicts: "Conflicts",
127
+ summaryPushed: "Changes pushed to GitHub",
128
+ summaryLocalOnly: "Saved locally, push is pending",
129
+ conflictsTitle: "What to choose?",
130
+ conflictsSubtitle: "The same record changed here and on GitHub. Choose what to keep.",
131
+ conflictMine: "Mine",
132
+ conflictRemote: "From GitHub",
133
+ conflictKeepMine: "Keep mine",
134
+ conflictTakeRemote: "Take from GitHub",
135
+ conflictMerge: "Merge",
136
+ hintKeepMine: "“Keep mine” — sends your version to GitHub",
137
+ hintTakeRemote: "“Take from GitHub” — replaces this file with the repository version",
138
+ hintMerge: "“Merge” — keeps both variants",
139
+ conflictWorking: "Applying…",
140
+ conflictDone: "Conflict resolved",
141
+ conflictFound: "Found",
142
+ previewEmpty: "empty",
143
+ projectsEmpty: "Open a project folder in DSH — its dossier will appear here automatically.",
144
+ statusSynced: "Synchronised",
145
+ statusLocalChanges: "Local changes",
146
+ statusConflict: "Conflict",
147
+ statusNotOpened: "Not opened locally",
148
+ historyOpen: "History",
149
+ historyTitle: "Version history",
150
+ historySubtitle: "Commits that changed this project dossier. Restore creates a new commit, so nothing is lost.",
151
+ historyEmpty: "No versions yet — the first sync will create them.",
152
+ historyLoading: "Loading history…",
153
+ historyClose: "Close",
154
+ historyRestore: "Restore",
155
+ historyRestoreHint: "Restores the dossier files that existed in this commit; newer files stay in place. Current files are backed up first.",
156
+ historyConfirm: "Restore the project dossier to this version? Current files will be backed up and a new commit will be created.",
157
+ historyDone: "Version restored",
158
+ historyAuthor: "by",
159
+ diffLegend: "Changed lines are highlighted: pink — only in mine, green — only on GitHub",
160
+ portableTitle: "USB transfer",
161
+ portableHeading: "Take project memory with you",
162
+ portableSubtitle: "Save dossiers to a USB drive and restore them on another machine — no GitHub, no commands.",
163
+ portablePlaceholder: "E:\\ (USB drive or folder)",
164
+ portableScan: "Scan",
165
+ portableExport: "Save to USB",
166
+ portableImport: "Import",
167
+ portableHint: "The drive contains redacted Markdown dossiers without secrets or AUTO-SCAN blocks. For sensitive data, enable BitLocker on the drive.",
168
+ portableEmpty: "No dsh-memory-portable folder found on this drive yet.",
169
+ portableFound: "{count} projects on the drive",
170
+ portableConfirm: "Import projects from the USB drive? Current dossiers will be backed up first.",
171
+ portableImported: "{count} projects imported",
172
+ portableExported: "{count} projects saved to the drive",
173
+ portableScanFirst: "Scan the USB drive first.",
174
+ portableNoPath: "Enter the USB drive path.",
175
+ portableNew: "new",
176
+ portableUpdate: "update",
177
+ portableSame: "up to date",
178
+ portableImportedState: "imported",
179
+ portableImportedNew: "restored",
180
+ portableSkipped: "skipped",
181
+ portableOnlyMemory: "Memory dossiers only",
182
+ portableCross: "Source code and workspace folders travel separately: Backup & transfer → Projects and code.",
183
+ portableStepSave: "Save",
184
+ portableStepScan: "Scan",
185
+ portableStepImport: "Restore",
186
+ };
187
+ const RU = {
188
+ nav: "Память проектов",
189
+ title: "Память проектов",
190
+ subtitle: "Досье памяти проектов синхронизируются через ваш собственный приватный GitHub-репозиторий. Команды не нужны — всё делается кнопками на этой странице.",
191
+ langProbe: "ru",
192
+ loading: "Читаю состояние синхронизации…",
193
+ loadError: "Не удалось получить состояние синхронизации",
194
+ retry: "Повторить",
195
+ errorPrefix: "Не удалось выполнить:",
196
+ syncErrorLabel: "Ошибка синхронизации",
197
+ never: "ещё не выполнялась",
198
+ secRepo: "Репозиторий",
199
+ secSync: "Синхронизация",
200
+ secHow: "Как это работает",
201
+ secSecurity: "Безопасность",
202
+ secConflicts: "Конфликты",
203
+ secProjects: "Проекты",
204
+ ghMissingTitle: "Нужен GitHub CLI",
205
+ ghMissingText: "Плагин работает через GitHub CLI. Установите его на этой машине и один раз войдите в аккаунт.",
206
+ ghInstallStep: "Установите GitHub CLI",
207
+ ghInstallHint: "Скачайте и установите GitHub CLI (gh) с cli.github.com.",
208
+ ghLoginStep: "Выполните в терминале",
209
+ ghLoginHint: "Вход нужен один раз на каждой машине.",
210
+ checkAgain: "Проверить снова",
211
+ checking: "Проверяю…",
212
+ ghLoginTitle: "Войдите в GitHub",
213
+ ghLoginText: "GitHub CLI установлен, но вход ещё не выполнен.",
214
+ repoSetupTitle: "Подключение репозитория",
215
+ repoSetupText: "Досье будут синхронизироваться через ваш собственный приватный репозиторий.",
216
+ ghLoggedIn: "GitHub: вы вошли как",
217
+ accountHint: "Репозиторий будет создан под вашим аккаунтом — ваши досье не попадут в чужие репозитории.",
218
+ repoLabel: "Репозиторий",
219
+ repoPlaceholder: "owner/name",
220
+ createRepo: "Создать приватный репозиторий",
221
+ creatingRepo: "Создаю…",
222
+ connectExisting: "Подключить существующий приватный",
223
+ connectingRepo: "Подключаю…",
224
+ repoFieldHint: "owner/name, только private; плагин проверит видимость.",
225
+ repoInvalid: "Укажите репозиторий в формате owner/name.",
226
+ privateOnlyNote: "Плагин отказывается работать с публичным репозиторием.",
227
+ configureDone: "Репозиторий подключён",
228
+ createDone: "Приватный репозиторий создан и подключён",
229
+ gitMissing: "Git не найден на этой машине. Без него синхронизация не сможет отправлять изменения.",
230
+ ghNotAuthorizedWarn: "GitHub CLI не авторизован — синхронизация не сможет отправлять изменения.",
231
+ stateSynced: "Всё синхронизировано",
232
+ stateLocalChanges: "Есть локальные изменения",
233
+ conflictOne: "конфликт",
234
+ conflictFew: "конфликта",
235
+ conflictMany: "конфликтов",
236
+ visPrivate: "Приватный",
237
+ visPublic: "ПУБЛИЧНЫЙ — отключите!",
238
+ visUnknown: "Видимость не подтверждена",
239
+ publicRepoWarn: "Репозиторий публичный. Отключите синхронизацию: досье памяти не должны быть видны посторонним.",
240
+ lastSync: "Последняя синхронизация",
241
+ branch: "Ветка",
242
+ pendingLocal: "Локальных правок",
243
+ pendingUnpushed: "Есть неотправленные изменения",
244
+ enabledBadge: "Включено",
245
+ disabledBadge: "Выключено",
246
+ syncNow: "Синхронизировать сейчас",
247
+ syncing: "Синхронизирую…",
248
+ enable: "Включить",
249
+ disable: "Выключить",
250
+ enabledDone: "Автоматическая синхронизация включена",
251
+ disabledDone: "Автоматическая синхронизация выключена",
252
+ openRepo: "Открыть репозиторий",
253
+ howButton: "Как это работает",
254
+ howShow: "Показать",
255
+ howHide: "Свернуть",
256
+ how1Title: "Приватный репозиторий",
257
+ how1Text: "Один приватный репозиторий на вашем аккаунте GitHub хранит общую копию досье памяти проектов.",
258
+ how2Title: "Трёхстороннее слияние",
259
+ how2Text: "Каждая машина при синхронизации сравнивает «моя версия / прошлая общая / версия с GitHub» и отправляет результат обратно.",
260
+ how3Title: "Ничего не затирается молча",
261
+ how3Text: "Дополнения с разных АРМ объединяются. Если одна и та же строка изменена по-разному, появляется конфликт — и вы выбираете вариант.",
262
+ sec1Title: "Только приватный репозиторий",
263
+ sec1Text: "Плагин проверяет видимость и отказывается работать с публичным репозиторием.",
264
+ sec2Title: "Секреты и PII вырезаются",
265
+ sec2Text: "Чувствительные данные удаляются, домашние пути обезличиваются.",
266
+ sec3Title: "Конфликты не перезаписывают файл",
267
+ sec3Text: "Пока вы не выбрали вариант, локальный файл остаётся нетронутым.",
268
+ sec4Title: "Бэкап и история Git",
269
+ sec4Text: "Каждое изменение сохраняется с резервной копией и видно в истории Git.",
270
+ optionsShow: "Настройки",
271
+ optionsHide: "Скрыть настройки",
272
+ optionsTitle: "Параметры синхронизации",
273
+ autoSync: "Автоматическая синхронизация",
274
+ autoSyncHint: "Синхронизировать при изменениях досье и по таймеру.",
275
+ intervalLabel: "Интервал",
276
+ secondsShort: "сек",
277
+ excludeLabel: "Исключённые проекты",
278
+ excludePlaceholder: "slug-один, slug-два",
279
+ excludeHint: "Слаги проектов через запятую. Их досье не синхронизируются.",
280
+ saveOptions: "Сохранить",
281
+ saving: "Сохраняю…",
282
+ optionsSaved: "Настройки сохранены",
283
+ syncDone: "Синхронизация завершена",
284
+ summaryProjects: "Проектов",
285
+ summaryFiles: "Файлов",
286
+ summaryConflicts: "Конфликтов",
287
+ summaryPushed: "Изменения отправлены в GitHub",
288
+ summaryLocalOnly: "Сохранено локально, отправка ещё впереди",
289
+ conflictsTitle: "Что выбрать?",
290
+ conflictsSubtitle: "Одна и та же запись изменилась здесь и на GitHub. Выберите, что оставить.",
291
+ conflictMine: "Моё",
292
+ conflictRemote: "С GitHub",
293
+ conflictKeepMine: "Оставить моё",
294
+ conflictTakeRemote: "Принять с GitHub",
295
+ conflictMerge: "Объединить",
296
+ hintKeepMine: "«Оставить моё» — отправит вашу версию в GitHub",
297
+ hintTakeRemote: "«Принять с GitHub» — заменит этот файл версией из репозитория",
298
+ hintMerge: "«Объединить» — сохранит оба варианта",
299
+ conflictWorking: "Применяю…",
300
+ conflictDone: "Конфликт разрешён",
301
+ conflictFound: "Обнаружен",
302
+ previewEmpty: "пусто",
303
+ projectsEmpty: "Откройте папку проекта в DSH — досье появится здесь автоматически.",
304
+ statusSynced: "Синхронизировано",
305
+ statusLocalChanges: "Есть локальные правки",
306
+ statusConflict: "Конфликт",
307
+ statusNotOpened: "Не открыт локально",
308
+ historyOpen: "История",
309
+ historyTitle: "История версий",
310
+ historySubtitle: "Commit’ы, которые меняли досье проекта. Восстановление создаёт новый commit — ничего не пропадает.",
311
+ historyEmpty: "Версий пока нет — первая синхронизация их создаст.",
312
+ historyLoading: "Загружаю историю…",
313
+ historyClose: "Закрыть",
314
+ historyRestore: "Восстановить",
315
+ historyRestoreHint: "Восстановит файлы досье, которые были в этом commit’е; файлы, появившиеся позже, останутся. Текущие файлы сначала сохраняются в бэкап.",
316
+ historyConfirm: "Восстановить досье проекта на эту версию? Текущие файлы будут сохранены в бэкап, затем создастся новый commit.",
317
+ historyDone: "Версия восстановлена",
318
+ historyAuthor: "автор",
319
+ diffLegend: "Изменённые строки подсвечены: розовые — только у меня, зелёные — только на GitHub",
320
+ portableTitle: "Перенос через флешку",
321
+ portableHeading: "Забрать память проектов с собой",
322
+ portableSubtitle: "Сохраните досье на флешку и восстановите на другой машине — без GitHub и команд.",
323
+ portablePlaceholder: "E:\\ (флешка или папка)",
324
+ portableScan: "Сканировать",
325
+ portableExport: "Сохранить на флешку",
326
+ portableImport: "Импортировать",
327
+ portableHint: "На флешке — обезличенные Markdown-досье без секретов и AUTO-SCAN. Для чувствительных данных включите BitLocker на флешке.",
328
+ portableEmpty: "На флешке пока нет папки dsh-memory-portable.",
329
+ portableFound: "На флешке проектов: {count}",
330
+ portableConfirm: "Импортировать проекты с флешки? Текущие досье сначала сохранятся в бэкап.",
331
+ portableImported: "Импортировано проектов: {count}",
332
+ portableExported: "Сохранено на флешку проектов: {count}",
333
+ portableScanFirst: "Сначала сканируйте флешку.",
334
+ portableNoPath: "Укажите путь к флешке.",
335
+ portableNew: "новый",
336
+ portableUpdate: "обновить",
337
+ portableSame: "актуально",
338
+ portableImportedState: "импортирован",
339
+ portableImportedNew: "восстановлен",
340
+ portableSkipped: "пропущен",
341
+ portableOnlyMemory: "Только досье памяти",
342
+ portableCross: "Исходники и рабочие папки переносятся отдельно: «Перенос и резервные копии → Проекты и код».",
343
+ portableStepSave: "Сохранить",
344
+ portableStepScan: "Сканировать",
345
+ portableStepImport: "Восстановить",
346
+ };
347
+ const ZH = {
348
+ nav: "项目记忆",
349
+ title: "项目记忆",
350
+ subtitle: "项目记忆档案通过你自己的私有 GitHub 仓库同步。无需命令——所有操作都在本页用按钮完成。",
351
+ langProbe: "zh",
352
+ loading: "正在读取同步状态…",
353
+ loadError: "无法读取同步状态",
354
+ retry: "重试",
355
+ errorPrefix: "无法完成:",
356
+ syncErrorLabel: "同步错误",
357
+ never: "尚未执行",
358
+ secRepo: "仓库",
359
+ secSync: "同步",
360
+ secHow: "工作原理",
361
+ secSecurity: "安全",
362
+ secConflicts: "冲突",
363
+ secProjects: "项目",
364
+ ghMissingTitle: "需要 GitHub CLI",
365
+ ghMissingText: "插件通过 GitHub CLI 工作。请在本机安装并登录一次。",
366
+ ghInstallStep: "安装 GitHub CLI",
367
+ ghInstallHint: "从 cli.github.com 下载并安装 GitHub CLI(gh)。",
368
+ ghLoginStep: "在终端中执行",
369
+ ghLoginHint: "每台机器只需登录一次。",
370
+ checkAgain: "再次检查",
371
+ checking: "检查中…",
372
+ ghLoginTitle: "登录 GitHub",
373
+ ghLoginText: "GitHub CLI 已安装,但尚未登录。",
374
+ repoSetupTitle: "连接仓库",
375
+ repoSetupText: "档案将通过你自己的私有仓库同步。",
376
+ ghLoggedIn: "GitHub:已登录为",
377
+ accountHint: "仓库将创建在你的账号下——你的档案不会进入别人的仓库。",
378
+ repoLabel: "仓库",
379
+ repoPlaceholder: "owner/name",
380
+ createRepo: "创建私有仓库",
381
+ creatingRepo: "创建中…",
382
+ connectExisting: "连接已有的私有仓库",
383
+ connectingRepo: "连接中…",
384
+ repoFieldHint: "owner/name,仅限 private;插件会检查可见性。",
385
+ repoInvalid: "请以 owner/name 格式填写仓库。",
386
+ privateOnlyNote: "插件拒绝使用公开仓库。",
387
+ configureDone: "仓库已连接",
388
+ createDone: "私有仓库已创建并连接",
389
+ gitMissing: "本机未找到 Git。没有它同步无法推送修改。",
390
+ ghNotAuthorizedWarn: "GitHub CLI 未授权——同步无法推送修改。",
391
+ stateSynced: "全部已同步",
392
+ stateLocalChanges: "有本地修改",
393
+ conflictOne: "个冲突",
394
+ conflictFew: "个冲突",
395
+ conflictMany: "个冲突",
396
+ visPrivate: "私有",
397
+ visPublic: "公开 — 请停用!",
398
+ visUnknown: "可见性未确认",
399
+ publicRepoWarn: "仓库是公开的。请停用同步:项目记忆档案不应对外可见。",
400
+ lastSync: "上次同步",
401
+ branch: "分支",
402
+ pendingLocal: "本地修改",
403
+ pendingUnpushed: "有未推送的修改",
404
+ enabledBadge: "已启用",
405
+ disabledBadge: "已禁用",
406
+ syncNow: "立即同步",
407
+ syncing: "同步中…",
408
+ enable: "启用",
409
+ disable: "禁用",
410
+ enabledDone: "自动同步已启用",
411
+ disabledDone: "自动同步已禁用",
412
+ openRepo: "打开仓库",
413
+ howButton: "工作原理",
414
+ howShow: "显示",
415
+ howHide: "收起",
416
+ how1Title: "私有仓库",
417
+ how1Text: "你 GitHub 账号下的一个私有仓库保存项目记忆档案的共享副本。",
418
+ how2Title: "三方合并",
419
+ how2Text: "每次同步时,机器会比较「我的版本 / 上次共同版本 / GitHub 上的版本」,并把结果推送回去。",
420
+ how3Title: "不会静默覆盖",
421
+ how3Text: "来自不同工作站的补充会合并。如果同一行被改得不一样,就会出现冲突,由你选择保留哪个版本。",
422
+ sec1Title: "仅限私有仓库",
423
+ sec1Text: "插件检查可见性,拒绝使用公开仓库。",
424
+ sec2Title: "敏感信息会被剔除",
425
+ sec2Text: "敏感数据会被删除,本机路径会被匿名化。",
426
+ sec3Title: "冲突不会覆盖文件",
427
+ sec3Text: "在你选择版本之前,本地文件保持不变。",
428
+ sec4Title: "备份与 Git 历史",
429
+ sec4Text: "每次修改都会保留备份,并可在 Git 历史中查看。",
430
+ optionsShow: "设置",
431
+ optionsHide: "收起设置",
432
+ optionsTitle: "同步设置",
433
+ autoSync: "自动同步",
434
+ autoSyncHint: "在档案变化时和按定时器同步。",
435
+ intervalLabel: "间隔",
436
+ secondsShort: "秒",
437
+ excludeLabel: "排除的项目",
438
+ excludePlaceholder: "slug-one, slug-two",
439
+ excludeHint: "项目 slug,用逗号分隔。这些档案不会被同步。",
440
+ saveOptions: "保存",
441
+ saving: "保存中…",
442
+ optionsSaved: "设置已保存",
443
+ syncDone: "同步完成",
444
+ summaryProjects: "项目",
445
+ summaryFiles: "文件",
446
+ summaryConflicts: "冲突",
447
+ summaryPushed: "修改已推送到 GitHub",
448
+ summaryLocalOnly: "已在本地保存,等待推送",
449
+ conflictsTitle: "要选哪个?",
450
+ conflictsSubtitle: "同一条记录在本机和 GitHub 上都发生了变化。请选择保留哪一份。",
451
+ conflictMine: "我的",
452
+ conflictRemote: "来自 GitHub",
453
+ conflictKeepMine: "保留我的",
454
+ conflictTakeRemote: "采用 GitHub 版本",
455
+ conflictMerge: "合并",
456
+ hintKeepMine: "「保留我的」——把你的版本发送到 GitHub",
457
+ hintTakeRemote: "「采用 GitHub 版本」——用仓库版本替换此文件",
458
+ hintMerge: "「合并」——保留两个版本",
459
+ conflictWorking: "正在应用…",
460
+ conflictDone: "冲突已解决",
461
+ conflictFound: "发现于",
462
+ previewEmpty: "空",
463
+ projectsEmpty: "在 DSH 中打开项目文件夹——档案会自动出现在这里。",
464
+ statusSynced: "已同步",
465
+ statusLocalChanges: "有本地修改",
466
+ statusConflict: "冲突",
467
+ statusNotOpened: "本地未打开",
468
+ historyOpen: "历史",
469
+ historyTitle: "版本历史",
470
+ historySubtitle: "修改此项目档案的提交。恢复会创建新提交,不会丢失任何内容。",
471
+ historyEmpty: "还没有版本——首次同步后会创建。",
472
+ historyLoading: "正在加载历史…",
473
+ historyClose: "关闭",
474
+ historyRestore: "恢复",
475
+ historyRestoreHint: "恢复该提交中存在的档案文件;之后新增的文件会保留。恢复前会先备份当前文件。",
476
+ historyConfirm: "将此项目档案恢复到该版本?当前文件会先备份,然后创建新提交。",
477
+ historyDone: "版本已恢复",
478
+ historyAuthor: "作者",
479
+ diffLegend: "变更行已高亮:粉色—仅本地,绿色—仅 GitHub",
480
+ portableTitle: "U 盘传输",
481
+ portableHeading: "随身携带项目记忆",
482
+ portableSubtitle: "将档案保存到 U 盘,在另一台机器上恢复——无需 GitHub 和命令。",
483
+ portablePlaceholder: "E:\\(U 盘或文件夹)",
484
+ portableScan: "扫描",
485
+ portableExport: "保存到 U 盘",
486
+ portableImport: "导入",
487
+ portableHint: "U 盘内是已脱敏的 Markdown 档案,不含密钥和 AUTO-SCAN。敏感数据请启用 BitLocker。",
488
+ portableEmpty: "U 盘上还没有 dsh-memory-portable 文件夹。",
489
+ portableFound: "U 盘上有 {count} 个项目",
490
+ portableConfirm: "从 U 盘导入项目?当前档案会先备份。",
491
+ portableImported: "已导入 {count} 个项目",
492
+ portableExported: "已保存 {count} 个项目到 U 盘",
493
+ portableScanFirst: "请先扫描 U 盘。",
494
+ portableNoPath: "请输入 U 盘路径。",
495
+ portableNew: "新项目",
496
+ portableUpdate: "可更新",
497
+ portableSame: "已是最新",
498
+ portableImportedState: "已导入",
499
+ portableImportedNew: "已恢复",
500
+ portableSkipped: "已跳过",
501
+ portableOnlyMemory: "仅项目记忆档案",
502
+ portableCross: "源码和工作区文件夹请使用「备份与传输 → 项目与代码」。",
503
+ portableStepSave: "保存",
504
+ portableStepScan: "扫描",
505
+ portableStepImport: "恢复",
506
+ };
507
+
508
+ const inject = ["slots", "locale", "connection"];
509
+
510
+ /* ------------------------------------------------------------------ */
511
+ /* inline SVG icons — no emoji, only currentColor strokes */
512
+ /* ------------------------------------------------------------------ */
513
+
514
+ const ICONS = {
515
+ check: [["path", { d: "M20 6 9 17l-5-5" }]],
516
+ alert: [["path", { d: "M12 3 22 21H2Z" }], ["path", { d: "M12 9v5" }], ["path", { d: "M12 17h.01" }]],
517
+ sync: [
518
+ ["path", { d: "M21 2v6h-6" }],
519
+ ["path", { d: "M3 12a9 9 0 0 1 15-6.7L21 8" }],
520
+ ["path", { d: "M3 22v-6h6" }],
521
+ ["path", { d: "M21 12a9 9 0 0 1-15 6.7L3 16" }],
522
+ ],
523
+ power: [["path", { d: "M18.36 6.64a9 9 0 1 1-12.73 0" }], ["path", { d: "M12 2v10" }]],
524
+ external: [
525
+ ["path", { d: "M14 4h6v6" }],
526
+ ["path", { d: "M20 4 11 13" }],
527
+ ["path", { d: "M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" }],
528
+ ],
529
+ help: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 2.5-3 4.5" }], ["path", { d: "M12 17h.01" }]],
530
+ gear: [
531
+ ["circle", { cx: "12", cy: "12", r: "3" }],
532
+ ["path", { d: "M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.55-1.11 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H9a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.55 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87V9a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1Z" }],
533
+ ],
534
+ chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
535
+ chevronUp: [["path", { d: "m6 15 6-6 6 6" }]],
536
+ terminal: [["rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }], ["path", { d: "m7 9 3 3-3 3" }], ["path", { d: "M13 15h4" }]],
537
+ user: [["path", { d: "M20 21a8 8 0 0 0-16 0" }], ["circle", { cx: "12", cy: "7", r: "4" }]],
538
+ shield: [["path", { d: "M12 3 19 6v5c0 4.5-3 8.5-7 10-4-1.5-7-5.5-7-10V6Z" }], ["path", { d: "m9 12 2 2 4-4" }]],
539
+ lock: [["rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }], ["path", { d: "M8 11V8a4 4 0 0 1 8 0v3" }]],
540
+ eyeOff: [
541
+ ["path", { d: "m3 3 18 18" }],
542
+ ["path", { d: "M10.6 10.6a2 2 0 0 0 2.8 2.8" }],
543
+ ["path", { d: "M9.4 5.3A9.5 9.5 0 0 1 12 5c5 0 9 4.5 9 7 0 .8-.4 1.7-1.2 2.6" }],
544
+ ["path", { d: "M6.1 6.9C3.9 8.4 3 10.5 3 12c0 2.5 4 7 9 7 1.3 0 2.5-.3 3.6-.8" }],
545
+ ],
546
+ file: [["path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z" }], ["path", { d: "M14 3v5h5" }]],
547
+ history: [["path", { d: "M3 12a9 9 0 1 0 3-6.7" }], ["path", { d: "M3 4v5h5" }], ["path", { d: "M12 8v4l3 2" }]],
548
+ plus: [["path", { d: "M12 5v14" }], ["path", { d: "M5 12h14" }]],
549
+ repo: [["path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" }]],
550
+ clock: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M12 7v5l3 2" }]],
551
+ dot: [["circle", { cx: "12", cy: "12", r: "3" }]],
552
+ usb: [
553
+ ["path", { d: "M12 3v11" }],
554
+ ["path", { d: "m8 10 4 4 4-4" }],
555
+ ["rect", { x: "7", y: "15", width: "10", height: "6", rx: "2" }],
556
+ ["path", { d: "M9 3h6" }],
557
+ ],
558
+ save: [
559
+ ["path", { d: "M4 4h12l4 4v12H4Z" }],
560
+ ["path", { d: "M8 4v6h8V4" }],
561
+ ["path", { d: "M8 20v-6h8v6" }],
562
+ ],
563
+ scan: [
564
+ ["path", { d: "M4 8V5a1 1 0 0 1 1-1h3" }],
565
+ ["path", { d: "M16 4h3a1 1 0 0 1 1 1v3" }],
566
+ ["path", { d: "M20 16v3a1 1 0 0 1-1 1h-3" }],
567
+ ["path", { d: "M8 20H5a1 1 0 0 1-1-1v-3" }],
568
+ ["path", { d: "M4 12h16" }],
569
+ ],
570
+ };
571
+
572
+ function Icon(props) {
573
+ const size = props.size ?? 16;
574
+ const shapes = ICONS[props.name] ?? ICONS.dot;
575
+ return h(
576
+ "svg",
577
+ {
578
+ className: "dmb-icon",
579
+ width: size,
580
+ height: size,
581
+ viewBox: "0 0 24 24",
582
+ fill: "none",
583
+ stroke: "currentColor",
584
+ strokeWidth: 1.7,
585
+ strokeLinecap: "round",
586
+ strokeLinejoin: "round",
587
+ "aria-hidden": "true",
588
+ focusable: "false",
589
+ },
590
+ shapes.map(([tag, attrs], index) => h(tag, { key: index, ...attrs })),
591
+ );
592
+ }
593
+
594
+ function Spinner() {
595
+ return h("span", { className: "dmb-spinner", "aria-hidden": "true" });
596
+ }
597
+
598
+ function Banner(props) {
599
+ const tone = props.tone ?? "ok";
600
+ const iconName = props.icon ?? (tone === "ok" ? "check" : tone === "info" ? "shield" : "alert");
601
+ return h(
602
+ "div",
603
+ { className: "dmb-status", "data-tone": tone, role: tone === "error" ? "alert" : "status" },
604
+ h(Icon, { name: iconName, size: 15 }),
605
+ h("span", { className: "dmb-status-text" }, props.children),
606
+ );
607
+ }
608
+
609
+ function Chip(props) {
610
+ return h(
611
+ "span",
612
+ { className: "dmb-chip", "data-tone": props.tone ?? "muted" },
613
+ props.icon !== undefined && props.icon !== null ? h(Icon, { name: props.icon, size: 12 }) : null,
614
+ h("span", null, props.children),
615
+ );
616
+ }
617
+
618
+ function Eyebrow(props) {
619
+ return h(
620
+ "div",
621
+ { className: "dmb-eyebrow" },
622
+ h("span", { className: "dmb-eyebrow-num" }, props.number),
623
+ h("span", { className: "dmb-eyebrow-label" }, props.label),
624
+ );
625
+ }
626
+
627
+ function CardHead(props) {
628
+ return h(
629
+ "div",
630
+ { className: "dmb-card-head" },
631
+ h(
632
+ "div",
633
+ { className: "dmb-card-head-main" },
634
+ h(Eyebrow, { number: props.number, label: props.label }),
635
+ props.title !== undefined && props.title !== null ? h("div", { className: "dmb-card-title" }, props.title) : null,
636
+ props.sub !== undefined && props.sub !== null ? h("div", { className: "dmb-card-sub" }, props.sub) : null,
637
+ ),
638
+ props.right !== undefined && props.right !== null ? props.right : null,
639
+ );
640
+ }
641
+
642
+ function KV(props) {
643
+ return h("div", { className: "dmb-kv" }, h("span", { className: "dmb-kv-label" }, props.label), h("span", { className: "dmb-kv-value" }, props.children));
644
+ }
645
+
646
+ /* ------------------------------------------------------------------ */
647
+ /* pure helpers */
648
+ /* ------------------------------------------------------------------ */
649
+
650
+ function errorText(error) {
651
+ if (error === undefined || error === null) return "unknown error";
652
+ if (typeof error === "string") return error;
653
+ if (typeof error.message === "string" && error.message.length > 0) return error.message;
654
+ if (error.error && typeof error.error.message === "string" && error.error.message.length > 0) return error.error.message;
655
+ try {
656
+ return JSON.stringify(error);
657
+ } catch {
658
+ return String(error);
659
+ }
660
+ }
661
+
662
+ /** The Connection carrier resolves envelopes: {ok:true, value} or {ok:false, error}. */
663
+ function assertOk(result) {
664
+ if (result !== null && typeof result === "object") {
665
+ if (result.ok === false) throw new Error(errorText(result.error ?? result.message));
666
+ if (result.value !== null && typeof result.value === "object" && result.value.ok === false) {
667
+ throw new Error(errorText(result.value.error ?? result.value.message));
668
+ }
669
+ }
670
+ return result;
671
+ }
672
+
673
+ function looksLikeStatus(value) {
674
+ if (value === null || typeof value !== "object") return false;
675
+ return typeof value.configured === "boolean" || Array.isArray(value.projects) || Array.isArray(value.conflicts);
676
+ }
677
+
678
+ function statusFrom(result) {
679
+ if (result === null || typeof result !== "object") return null;
680
+ if (result.status !== null && typeof result.status === "object" && looksLikeStatus(result.status)) return result.status;
681
+ if (looksLikeStatus(result)) return result;
682
+ if (result.value !== null && typeof result.value === "object") {
683
+ if (result.value.status !== null && typeof result.value.status === "object" && looksLikeStatus(result.value.status)) return result.value.status;
684
+ if (looksLikeStatus(result.value)) return result.value;
685
+ }
686
+ return null;
687
+ }
688
+
689
+ function historyFrom(result) {
690
+ if (result === null || typeof result !== "object") return [];
691
+ if (Array.isArray(result.commits)) return result.commits;
692
+ if (result.value !== null && typeof result.value === "object" && Array.isArray(result.value.commits)) return result.value.commits;
693
+ return [];
694
+ }
695
+
696
+ /** Line-level diff for the two conflict previews (pink = only mine, green = only GitHub). */
697
+ function diffRows(text, other) {
698
+ const mine = String(text ?? "").split("\n");
699
+ const theirs = String(other ?? "").split("\n");
700
+ const n = mine.length;
701
+ const m = theirs.length;
702
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
703
+ for (let i = n - 1; i >= 0; i -= 1) {
704
+ for (let j = m - 1; j >= 0; j -= 1) {
705
+ dp[i][j] = mine[i] === theirs[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
706
+ }
707
+ }
708
+ const rows = [];
709
+ let i = 0;
710
+ let j = 0;
711
+ while (i < n && j < m) {
712
+ if (mine[i] === theirs[j]) {
713
+ rows.push({ text: mine[i], changed: false });
714
+ i += 1;
715
+ j += 1;
716
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
717
+ rows.push({ text: mine[i], changed: true });
718
+ i += 1;
719
+ } else {
720
+ j += 1;
721
+ }
722
+ }
723
+ while (i < n) {
724
+ rows.push({ text: mine[i], changed: true });
725
+ i += 1;
726
+ }
727
+ return rows;
728
+ }
729
+
730
+ function formatTime(value) {
731
+ if (typeof value !== "string" || value.length === 0) return null;
732
+ const date = new Date(value);
733
+ if (Number.isNaN(date.getTime())) return value;
734
+ try {
735
+ return date.toLocaleString(undefined, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
736
+ } catch {
737
+ return value;
738
+ }
739
+ }
740
+
741
+ /** “5 минут назад” — localised through Intl.RelativeTimeFormat. */
742
+ function relativeTime(value, lang) {
743
+ if (typeof value !== "string" || value.length === 0) return null;
744
+ const date = new Date(value);
745
+ if (Number.isNaN(date.getTime())) return value;
746
+ const diffSeconds = Math.round((date.getTime() - Date.now()) / 1000);
747
+ const abs = Math.abs(diffSeconds);
748
+ let unit = "second";
749
+ let divisor = 1;
750
+ if (abs >= 31536000) {
751
+ unit = "year";
752
+ divisor = 31536000;
753
+ } else if (abs >= 2592000) {
754
+ unit = "month";
755
+ divisor = 2592000;
756
+ } else if (abs >= 86400) {
757
+ unit = "day";
758
+ divisor = 86400;
759
+ } else if (abs >= 3600) {
760
+ unit = "hour";
761
+ divisor = 3600;
762
+ } else if (abs >= 60) {
763
+ unit = "minute";
764
+ divisor = 60;
765
+ }
766
+ try {
767
+ return new Intl.RelativeTimeFormat(lang, { numeric: "auto" }).format(Math.round(diffSeconds / divisor), unit);
768
+ } catch {
769
+ return formatTime(value) ?? value;
770
+ }
771
+ }
772
+
773
+ function parseSlugs(value) {
774
+ const seen = Object.create(null);
775
+ const result = [];
776
+ String(value ?? "").split(/[,;\s]+/u).forEach((raw) => {
777
+ const slug = raw.trim().toLowerCase();
778
+ if (slug.length === 0) return;
779
+ if (seen[slug] === true) return;
780
+ seen[slug] = true;
781
+ result.push(slug);
782
+ });
783
+ return result;
784
+ }
785
+
786
+ function optionsFromStatus(value) {
787
+ const options = value !== null && typeof value === "object" && value.options !== null && typeof value.options === "object" ? value.options : {};
788
+ const interval = Number(options.intervalSeconds);
789
+ return {
790
+ autoSync: options.autoSync === true,
791
+ intervalSeconds: ALLOWED_INTERVALS.includes(interval) ? String(interval) : "300",
792
+ excludeProjects: Array.isArray(options.excludeProjects) ? options.excludeProjects.join(", ") : "",
793
+ };
794
+ }
795
+
796
+ function emptyDraft() {
797
+ return { autoSync: true, intervalSeconds: "300", excludeProjects: "" };
798
+ }
799
+
800
+ /** Suggested repo comes from the Host: `<login>/dsh-memory-exchange`, never hard-coded. */
801
+ function suggestedRepoFrom(status) {
802
+ if (status === null || typeof status !== "object") return null;
803
+ const github = status.github !== null && typeof status.github === "object" ? status.github : null;
804
+ const repo =
805
+ github !== null && typeof github.suggestedRepo === "string"
806
+ ? github.suggestedRepo
807
+ : typeof status.suggestedRepo === "string"
808
+ ? status.suggestedRepo
809
+ : "";
810
+ return repo.length > 0 ? repo : null;
811
+ }
812
+
813
+ function githubFrom(status) {
814
+ const raw = status !== null && typeof status === "object" && status.github !== null && typeof status.github === "object" ? status.github : null;
815
+ return {
816
+ available: raw !== null && raw.available === true,
817
+ authenticated: raw !== null && raw.authenticated === true,
818
+ login: raw !== null && typeof raw.login === "string" && raw.login.length > 0 ? raw.login : null,
819
+ suggestedRepo: suggestedRepoFrom(status),
820
+ };
821
+ }
822
+
823
+ /** "private" | "public" | "unknown" — Host sends `repoVisibility`/`visibility`. */
824
+ function visibilityFrom(status) {
825
+ if (status === null || typeof status !== "object") return "unknown";
826
+ const repo = status.repo !== null && typeof status.repo === "object" ? status.repo : null;
827
+ const github = status.github !== null && typeof status.github === "object" ? status.github : null;
828
+ const candidates = [
829
+ status.repoVisibility,
830
+ status.visibility,
831
+ github !== null ? github.repoVisibility : null,
832
+ status.repository !== null && typeof status.repository === "object" ? status.repository.visibility : null,
833
+ status.repoInfo !== null && typeof status.repoInfo === "object" ? status.repoInfo.visibility : null,
834
+ repo !== null ? repo.visibility : null,
835
+ ];
836
+ for (const candidate of candidates) {
837
+ const value = String(candidate ?? "").toLowerCase();
838
+ if (value === "private") return "private";
839
+ if (value === "public") return "public";
840
+ }
841
+ if (status.private === true || status.isPrivate === true || (repo !== null && repo.private === true)) return "private";
842
+ if (status.private === false || status.isPrivate === false || (repo !== null && repo.private === false)) return "public";
843
+ return "unknown";
844
+ }
845
+
846
+ const PROJECT_STATUS_KEY = {
847
+ synced: "statusSynced",
848
+ "local-changes": "statusLocalChanges",
849
+ conflict: "statusConflict",
850
+ "not-opened": "statusNotOpened",
851
+ };
852
+
853
+ function projectStatusKey(value) {
854
+ return PROJECT_STATUS_KEY[String(value ?? "")] ?? "statusNotOpened";
855
+ }
856
+
857
+ function projectStatusTone(value) {
858
+ if (value === "synced") return "ok";
859
+ if (value === "local-changes") return "warn";
860
+ if (value === "conflict") return "error";
861
+ return "muted";
862
+ }
863
+
864
+ /* ------------------------------------------------------------------ */
865
+ /* plugin */
866
+ /* ------------------------------------------------------------------ */
867
+
868
+ function apply(ctx) {
869
+ const t = ctx.locale.bind(NS);
870
+
871
+ const callbacks = {
872
+ call: async (operation, payload) => {
873
+ let signal;
874
+ try {
875
+ signal = AbortSignal.timeout(180000);
876
+ } catch {
877
+ signal = undefined;
878
+ }
879
+ return await ctx.connection.rpc.call("/api", `${RPC_NS}/${operation}`, payload ?? {}, signal);
880
+ },
881
+ openUrl: (url) => {
882
+ try {
883
+ window.open(url, "_blank", "noopener,noreferrer");
884
+ return true;
885
+ } catch {
886
+ return false;
887
+ }
888
+ },
889
+ };
890
+
891
+ ctx.effect(() => ctx.locale.register(NS, { en: EN, zh: ZH }), "memory-bridge: dictionaries");
892
+ ctx.effect(() => {
893
+ try {
894
+ return ctx.locale.register(NS, "ru", RU);
895
+ } catch {
896
+ return () => {};
897
+ }
898
+ }, "memory-bridge: ru dictionary");
899
+
900
+ ctx.effect(() => {
901
+ const style = document.createElement("style");
902
+ style.id = "dsh-memory-bridge-style";
903
+ style.textContent = `
904
+ .dmb-page{display:flex;flex-direction:column;gap:18px;max-width:760px;padding:2px 0 24px;box-sizing:border-box;color:var(--dsw-alias-label-primary,#111)}
905
+ .dmb-head{display:flex;flex-direction:column;gap:6px}
906
+ .dmb-kicker{display:flex;align-items:center;gap:10px;font-size:11px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#6b7280)}
907
+ .dmb-kicker::before{content:"";width:28px;height:1px;background:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
908
+ .dmb-title{font-size:22px;line-height:28px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
909
+ .dmb-sub{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,#5b6472);max-width:66ch}
910
+ .dmb-banners{display:flex;flex-direction:column;gap:8px}
911
+ .dmb-card{display:flex;flex-direction:column;gap:14px;padding:18px 18px 16px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));border-radius:14px;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.02));box-sizing:border-box}
912
+ .dmb-card--flat{gap:12px;padding:16px 18px}
913
+ .dmb-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
914
+ .dmb-card-head-main{display:flex;flex-direction:column;gap:4px;min-width:0}
915
+ .dmb-card-title{font-size:16px;line-height:22px;font-weight:650;color:var(--dsw-alias-label-primary,#111);margin-top:2px}
916
+ .dmb-card-sub{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472);max-width:62ch}
917
+ .dmb-eyebrow{display:flex;align-items:center;gap:8px;font-size:10.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#6b7280)}
918
+ .dmb-eyebrow-num{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 5px;border-radius:6px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,#4b5563);font-variant-numeric:tabular-nums}
919
+ .dmb-icon{flex:none;display:block}
920
+ .dmb-chip{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;letter-spacing:.03em;padding:3px 9px;border-radius:999px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,#4b5563);font-variant-numeric:tabular-nums;white-space:nowrap}
921
+ .dmb-chip[data-tone="ok"]{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2e9e5b) 14%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2e9e5b) 28%,transparent);color:var(--dsw-alias-state-success-primary,#2e9e5b)}
922
+ .dmb-chip[data-tone="warn"]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 14%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 28%,transparent);color:var(--dsw-alias-state-warn-primary,#b45309)}
923
+ .dmb-chip[data-tone="error"]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d64545) 14%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d64545) 30%,transparent);color:var(--dsw-alias-state-error-primary,#d64545);letter-spacing:.05em}
924
+ .dmb-chip[data-tone="info"]{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 12%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 26%,transparent);color:var(--dsw-alias-brand-primary,#2f7df6)}
925
+ .dmb-state{display:flex;align-items:flex-start;gap:12px;padding:14px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);box-sizing:border-box}
926
+ .dmb-state-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:10px;flex:none;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,#4b5563)}
927
+ .dmb-state[data-tone="ok"] .dmb-state-icon{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2e9e5b) 14%,transparent);color:var(--dsw-alias-state-success-primary,#2e9e5b)}
928
+ .dmb-state[data-tone="warn"] .dmb-state-icon{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 14%,transparent);color:var(--dsw-alias-state-warn-primary,#b45309)}
929
+ .dmb-state[data-tone="error"] .dmb-state-icon{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d64545) 14%,transparent);color:var(--dsw-alias-state-error-primary,#d64545)}
930
+ .dmb-state-body{display:flex;flex-direction:column;gap:6px;min-width:0}
931
+ .dmb-state-title{font-size:18px;line-height:24px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
932
+ .dmb-state-meta{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#5b6472)}
933
+ .dmb-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472)}
934
+ .dmb-sep{color:var(--dsw-alias-border-l2,rgba(0,0,0,.24))}
935
+ .dmb-link{color:var(--dsw-alias-brand-primary,#2f7df6);text-decoration:none;font-weight:600;font-size:12.5px;line-height:18px;word-break:break-all}
936
+ .dmb-link:hover{text-decoration:underline}
937
+ .dmb-mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:17px;word-break:break-all;color:var(--dsw-alias-label-primary,#111)}
938
+ .dmb-repo-line{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472)}
939
+ .dmb-account{display:flex;align-items:center;gap:8px;font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
940
+ .dmb-account .dmb-icon{color:var(--dsw-alias-brand-primary,#2f7df6)}
941
+ .dmb-note{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#5b6472)}
942
+ .dmb-note .dmb-icon{margin-top:1px}
943
+ .dmb-code{display:block;width:100%;box-sizing:border-box;margin-top:6px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.05));border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:17px;color:var(--dsw-alias-label-primary,#111);overflow-x:auto;white-space:nowrap}
944
+ .dmb-field{display:flex;flex-direction:column;gap:6px}
945
+ .dmb-label{font-size:11.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#6b7280)}
946
+ .dmb-input,.dmb-select{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;line-height:18px}
947
+ .dmb-portable-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0}
948
+ .dmb-portable-row .dmb-input{flex:1;min-width:220px}
949
+ .dmb-portable-row .dmb-btn{flex:none}
950
+ .dmb-portable-projects{margin-top:10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));padding-top:8px}
951
+ .dmb-portable-hint{margin-top:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
952
+ .dmb-usb{background:radial-gradient(120% 160% at 100% 0%,color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 7%,transparent) 0%,transparent 55%),var(--dsw-alias-bg-base,#fff);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 18%,var(--dsw-alias-border-l1,rgba(0,0,0,.1)))}
953
+ .dmb-usb .dmb-card-head{flex-wrap:wrap;gap:8px}
954
+ .dmb-usb .dmb-card-head-main{min-width:0}
955
+ .dmb-usb-badge{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 24%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 7%,transparent);color:var(--dsw-alias-brand-primary,#2f7df6);font-size:10.5px;font-weight:650;white-space:nowrap}
956
+ .dmb-usb-steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:2px 0 2px}
957
+ .dmb-usb-step{display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 4%,transparent);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
958
+ .dmb-usb-step-icon{display:inline-flex;color:var(--dsw-alias-brand-primary,#2f7df6)}
959
+ .dmb-usb-step-label{font-size:11px;line-height:14px;font-weight:650;color:var(--dsw-alias-label-secondary,#5b6472)}
960
+ .dmb-usb-step-arrow{margin-left:2px;color:var(--dsw-alias-label-secondary,#9aa0a9);font-size:11px}
961
+ .dmb-usb-field{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));border-radius:10px;background:var(--dsw-alias-bg-base,#fff);padding:0 10px;transition:border-color .16s ease,box-shadow .16s ease}
962
+ .dmb-usb-field:focus-within{border-color:var(--dsw-alias-brand-primary,#2f7df6);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 12%,transparent)}
963
+ .dmb-usb-field-icon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary,#6b7280)}
964
+ .dmb-usb-field .dmb-input{border:none;background:transparent;padding-left:0;box-shadow:none;outline:none}
965
+ .dmb-usb-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
966
+ .dmb-usb-actions .dmb-btn{min-height:32px;padding:0 12px;font-size:12px}
967
+ .dmb-usb-cross{display:flex;align-items:flex-start;gap:7px;margin-top:4px;padding-top:8px;border-top:1px dashed var(--dsw-alias-border-l1,rgba(0,0,0,.08));font-size:10.5px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
968
+ .dmb-usb-cross .dmb-icon{flex:none;margin-top:1px;color:var(--dsw-alias-label-secondary,#8a9098)}
969
+ @media (max-width:560px){.dmb-usb-steps{gap:5px}.dmb-usb-step{padding:4px 7px}.dmb-usb-step-arrow{display:none}.dmb-usb-actions .dmb-btn{flex:1 1 100%}}
970
+ .dmb-input--mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:17px}
971
+ .dmb-input:focus-visible,.dmb-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
972
+ .dmb-input:disabled,.dmb-select:disabled{opacity:.6}
973
+ .dmb-hint{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary,#6b7280)}
974
+ .dmb-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
975
+ .dmb-btn{border-radius:10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:transparent;color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;font-weight:600;line-height:18px;padding:8px 14px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;transition:background .15s ease,border-color .15s ease,opacity .15s ease,filter .15s ease}
976
+ .dmb-btn:hover:not([disabled]){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
977
+ .dmb-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
978
+ .dmb-btn[data-tone="primary"]{background:var(--dsw-alias-label-primary,#111);border-color:transparent;color:var(--dsw-alias-bg-base,#fff)}
979
+ .dmb-btn[data-tone="primary"]:hover:not([disabled]){filter:brightness(1.06);background:var(--dsw-alias-label-primary,#111)}
980
+ .dmb-btn[data-tone="caution"]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 40%,transparent);color:var(--dsw-alias-state-warn-primary,#b45309)}
981
+ .dmb-btn[data-tone="caution"]:hover:not([disabled]){background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 8%,transparent)}
982
+ .dmb-btn--ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary,#5b6472);padding:6px 10px;min-height:30px;font-size:12px}
983
+ .dmb-btn--ghost:hover:not([disabled]){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-primary,#111)}
984
+ .dmb-btn--small{padding:6px 10px;min-height:30px;font-size:12px}
985
+ .dmb-btn[disabled]{opacity:.55;cursor:default}
986
+ .dmb-spinner{width:13px;height:13px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;animation:dmb-spin .8s linear infinite;flex:none}
987
+ @keyframes dmb-spin{to{transform:rotate(360deg)}}
988
+ .dmb-status{display:flex;align-items:flex-start;gap:8px;font-size:12.5px;line-height:18px;padding:10px 12px;border-radius:10px;border:1px solid transparent;color:var(--dsw-alias-label-primary,#111);box-sizing:border-box}
989
+ .dmb-status .dmb-icon{margin-top:1px}
990
+ .dmb-status[data-tone="ok"]{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2e9e5b) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2e9e5b) 30%,transparent)}
991
+ .dmb-status[data-tone="error"]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d64545) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d64545) 30%,transparent)}
992
+ .dmb-status[data-tone="warn"]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 30%,transparent)}
993
+ .dmb-status[data-tone="info"]{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 30%,transparent)}
994
+ .dmb-status-text{min-width:0}
995
+ .dmb-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
996
+ .dmb-step{display:flex;gap:12px;align-items:flex-start}
997
+ .dmb-step-num{flex:none;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-brand-primary,#2f7df6);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums}
998
+ .dmb-step-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
999
+ .dmb-step-title{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
1000
+ .dmb-step-text{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#5b6472)}
1001
+ .dmb-security{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:12px}
1002
+ .dmb-security li{display:flex;gap:10px;align-items:flex-start;min-width:0}
1003
+ .dmb-security-icon{color:var(--dsw-alias-label-secondary,#6b7280);margin-top:1px}
1004
+ .dmb-security-title{font-size:12.5px;line-height:17px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
1005
+ .dmb-security-text{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary,#5b6472)}
1006
+ .dmb-conflict{display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);box-sizing:border-box}
1007
+ .dmb-conflict-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
1008
+ .dmb-conflict-title{font-size:13.5px;line-height:19px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
1009
+ .dmb-path{margin:0}
1010
+ .dmb-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
1011
+ .dmb-grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
1012
+ .dmb-preview{display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);min-width:0;box-sizing:border-box}
1013
+ .dmb-pre{margin:0;padding:10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.05));color:var(--dsw-alias-label-primary,#111);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;line-height:16px;white-space:pre-wrap;word-break:break-word;max-height:220px;overflow:auto;box-sizing:border-box}
1014
+ .dmb-diff-line{display:block;min-height:1.15em}
1015
+ .dmb-diff-del{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 16%,transparent);border-radius:3px}
1016
+ .dmb-diff-add{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 16%,transparent);border-radius:3px}
1017
+ .dmb-btn--sm{min-height:28px;padding:0 10px;font-size:12px;border-radius:8px;font-weight:600}
1018
+ .dmb-diff-legend{margin-top:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
1019
+ .dmb-kv{display:flex;flex-direction:column;gap:2px;min-width:0}
1020
+ .dmb-kv-label{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
1021
+ .dmb-kv-value{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums}
1022
+ .dmb-hints{display:flex;flex-direction:column;gap:4px}
1023
+ .dmb-hint-line{display:flex;align-items:flex-start;gap:6px;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary,#6b7280)}
1024
+ .dmb-hint-line .dmb-icon{margin-top:1px;color:var(--dsw-alias-label-secondary,#6b7280)}
1025
+ .dmb-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1026
+ .dmb-row{display:flex;align-items:center;gap:12px;padding:10px 2px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1027
+ .dmb-row:last-child{border-bottom:none}
1028
+ .dmb-row-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
1029
+ .dmb-row-title{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
1030
+ .dmb-row-desc{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
1031
+ .dmb-row-time{flex:none;font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-secondary,#6b7280);font-variant-numeric:tabular-nums;white-space:nowrap}
1032
+ .dmb-empty{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);padding:6px 0}
1033
+ .dmb-loading{display:flex;align-items:center;gap:10px;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);padding:4px 0}
1034
+ .dmb-divider{height:1px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.08));margin:2px 0}
1035
+ .dmb-options{display:flex;flex-direction:column;gap:14px}
1036
+ .dmb-check{display:flex;align-items:flex-start;gap:10px;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-primary,#111);cursor:pointer}
1037
+ .dmb-check input{margin-top:1px;accent-color:var(--dsw-alias-brand-primary,#2f7df6);width:15px;height:15px;flex:none}
1038
+ .dmb-check-text{display:flex;flex-direction:column;gap:2px;min-width:0}
1039
+ .dmb-check-title{font-weight:600}
1040
+ @media (max-width:560px){
1041
+ .dmb-page{gap:14px}
1042
+ .dmb-card{padding:14px}
1043
+ .dmb-grid2,.dmb-grid3,.dmb-security{grid-template-columns:1fr}
1044
+ .dmb-actions{flex-direction:column;align-items:stretch}
1045
+ .dmb-actions .dmb-btn{width:100%}
1046
+ .dmb-card-head{flex-direction:column;align-items:flex-start;gap:8px}
1047
+ .dmb-row{flex-wrap:wrap;align-items:flex-start}
1048
+ .dmb-row-text{flex:1 1 100%}
1049
+ .dmb-row .dmb-chip{order:1}
1050
+ .dmb-row-time{order:2;margin-left:auto}
1051
+ .dmb-row .dmb-btn{order:3;flex:1 1 100%;margin-top:4px}
1052
+ .dmb-row-title,.dmb-row-desc{overflow-wrap:break-word;word-break:normal}
1053
+ .dmb-state-title{font-size:16px;line-height:22px}
1054
+ }
1055
+ @media (prefers-reduced-motion:reduce){.dmb-spinner{animation-duration:2.4s}}
1056
+ `;
1057
+ document.head.appendChild(style);
1058
+ return () => style.remove();
1059
+ }, "memory-bridge: styles");
1060
+
1061
+ const call = callbacks.call;
1062
+ const openUrl = callbacks.openUrl;
1063
+
1064
+ function MemorySyncSection(props) {
1065
+ const rpc = props.call ?? call;
1066
+ const [phase, setPhase] = useState("loading");
1067
+ const [status, setStatus] = useState(null);
1068
+ const [busy, setBusy] = useState(null);
1069
+ const [message, setMessage] = useState(null);
1070
+ const [summary, setSummary] = useState(null);
1071
+ const [repoInput, setRepoInput] = useState("");
1072
+ const [optionsOpen, setOptionsOpen] = useState(false);
1073
+ const [draft, setDraft] = useState(emptyDraft);
1074
+ const [howOpen, setHowOpen] = useState(false);
1075
+ const [historyFor, setHistoryFor] = useState(null);
1076
+ const [historyItems, setHistoryItems] = useState(null);
1077
+ const [historyLoading, setHistoryLoading] = useState(false);
1078
+ const [portablePath, setPortablePath] = useState("");
1079
+ const [portableBusy, setPortableBusy] = useState(null);
1080
+ const [portableResult, setPortableResult] = useState(null);
1081
+ const [portableMessage, setPortableMessage] = useState(null);
1082
+ const repoEditedRef = useRef(false);
1083
+ const lastSuggestedRef = useRef(null);
1084
+ const aliveRef = useRef(true);
1085
+
1086
+ const lang = (() => {
1087
+ const value = t("langProbe");
1088
+ return typeof value === "string" && value.length > 0 ? value : "en";
1089
+ })();
1090
+
1091
+ function pluralWord(count, oneKey, fewKey, manyKey) {
1092
+ const n = Number(count) || 0;
1093
+ let word;
1094
+ if (lang === "ru") {
1095
+ const mod10 = n % 10;
1096
+ const mod100 = n % 100;
1097
+ if (mod10 === 1 && mod100 !== 11) word = t(oneKey);
1098
+ else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) word = t(fewKey);
1099
+ else word = t(manyKey);
1100
+ } else {
1101
+ word = n === 1 ? t(oneKey) : t(manyKey);
1102
+ }
1103
+ return `${n} ${word}`;
1104
+ }
1105
+
1106
+ async function loadStatus(silent) {
1107
+ try {
1108
+ const result = assertOk(await rpc("status", {}));
1109
+ if (aliveRef.current !== true) return false;
1110
+ const next = statusFrom(result);
1111
+ if (next === null) {
1112
+ if (silent !== true) {
1113
+ setPhase("error");
1114
+ setMessage({ tone: "error", text: t("loadError") });
1115
+ }
1116
+ return false;
1117
+ }
1118
+ setStatus(next);
1119
+ setPhase("ready");
1120
+ const suggested = suggestedRepoFrom(next);
1121
+ if (repoEditedRef.current !== true) {
1122
+ setRepoInput((current) => {
1123
+ if (current.length === 0) return suggested ?? "";
1124
+ if (lastSuggestedRef.current !== null && current === lastSuggestedRef.current) return suggested ?? current;
1125
+ return current;
1126
+ });
1127
+ }
1128
+ if (suggested !== null) lastSuggestedRef.current = suggested;
1129
+ return true;
1130
+ } catch (error) {
1131
+ if (aliveRef.current !== true) return false;
1132
+ setPhase((current) => (current === "ready" ? current : "error"));
1133
+ if (silent !== true) setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1134
+ return false;
1135
+ }
1136
+ }
1137
+
1138
+ useEffect(() => {
1139
+ aliveRef.current = true;
1140
+ void loadStatus(false);
1141
+ const timer = setInterval(() => {
1142
+ void loadStatus(true);
1143
+ }, 10000);
1144
+ return () => {
1145
+ aliveRef.current = false;
1146
+ clearInterval(timer);
1147
+ };
1148
+ }, []);
1149
+
1150
+ function takeStatus(result) {
1151
+ const next = statusFrom(result);
1152
+ if (next !== null) {
1153
+ setStatus(next);
1154
+ setPhase("ready");
1155
+ }
1156
+ return next;
1157
+ }
1158
+
1159
+ function toggleOptions() {
1160
+ if (optionsOpen === true) {
1161
+ setOptionsOpen(false);
1162
+ return;
1163
+ }
1164
+ setDraft(optionsFromStatus(status));
1165
+ setOptionsOpen(true);
1166
+ }
1167
+
1168
+ function toggleHow() {
1169
+ setHowOpen((current) => current !== true);
1170
+ }
1171
+
1172
+ function openRepo() {
1173
+ const url = status !== null && typeof status === "object" && typeof status.repoUrl === "string" ? status.repoUrl : "";
1174
+ if (url.length === 0) return;
1175
+ openUrl(url);
1176
+ }
1177
+
1178
+ async function doCheck() {
1179
+ if (busy !== null) return;
1180
+ setBusy("check");
1181
+ setMessage(null);
1182
+ try {
1183
+ await loadStatus(false);
1184
+ } finally {
1185
+ setBusy(null);
1186
+ }
1187
+ }
1188
+
1189
+ async function doConnect(kind) {
1190
+ if (busy !== null) return;
1191
+ const repo = repoInput.trim();
1192
+ if (kind === "configure" && repo.length === 0) {
1193
+ setMessage({ tone: "error", text: t("repoInvalid") });
1194
+ return;
1195
+ }
1196
+ if (repo.length > 0 && REPO_PATTERN.test(repo) !== true) {
1197
+ setMessage({ tone: "error", text: t("repoInvalid") });
1198
+ return;
1199
+ }
1200
+ const busyKey = kind === "create" ? "create" : "configure";
1201
+ setBusy(busyKey);
1202
+ setMessage(null);
1203
+ setSummary(null);
1204
+ try {
1205
+ const payload = kind === "create" ? (repo.length > 0 ? { repo } : {}) : { repo };
1206
+ const result = assertOk(await rpc(kind === "create" ? "create-repo" : "configure", payload));
1207
+ const next = takeStatus(result);
1208
+ if (next !== null) setDraft(optionsFromStatus(next));
1209
+ const created =
1210
+ result !== null && typeof result === "object" && result.value !== null && typeof result.value === "object" ? result.value.created !== false : true;
1211
+ setMessage({ tone: "ok", text: kind === "create" ? (created === true ? t("createDone") : t("configureDone")) : t("configureDone") });
1212
+ await loadStatus(true);
1213
+ } catch (error) {
1214
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1215
+ } finally {
1216
+ setBusy(null);
1217
+ }
1218
+ }
1219
+
1220
+ async function doSync() {
1221
+ if (busy !== null) return;
1222
+ setBusy("sync");
1223
+ setMessage(null);
1224
+ setSummary(null);
1225
+ try {
1226
+ const result = assertOk(await rpc("sync-now", {}));
1227
+ takeStatus(result);
1228
+ setSummary(summaryFrom(result));
1229
+ setMessage({ tone: "ok", text: t("syncDone") });
1230
+ await loadStatus(true);
1231
+ } catch (error) {
1232
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1233
+ await loadStatus(true);
1234
+ } finally {
1235
+ setBusy(null);
1236
+ }
1237
+ }
1238
+
1239
+ async function doToggle() {
1240
+ if (busy !== null || status === null) return;
1241
+ const enable = status.enabled !== true;
1242
+ setBusy("toggle");
1243
+ setMessage(null);
1244
+ setSummary(null);
1245
+ try {
1246
+ const result = assertOk(await rpc("set-enabled", { enabled: enable }));
1247
+ takeStatus(result);
1248
+ setMessage({ tone: "ok", text: enable === true ? t("enabledDone") : t("disabledDone") });
1249
+ await loadStatus(true);
1250
+ } catch (error) {
1251
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1252
+ } finally {
1253
+ setBusy(null);
1254
+ }
1255
+ }
1256
+
1257
+ async function doSaveOptions() {
1258
+ if (busy !== null) return;
1259
+ const interval = Number(draft.intervalSeconds);
1260
+ setBusy("options");
1261
+ setMessage(null);
1262
+ setSummary(null);
1263
+ try {
1264
+ const result = assertOk(await rpc("set-options", {
1265
+ autoSync: draft.autoSync === true,
1266
+ intervalSeconds: ALLOWED_INTERVALS.includes(interval) ? interval : 300,
1267
+ excludeProjects: parseSlugs(draft.excludeProjects),
1268
+ }));
1269
+ const next = takeStatus(result);
1270
+ if (next !== null) setDraft(optionsFromStatus(next));
1271
+ setMessage({ tone: "ok", text: t("optionsSaved") });
1272
+ await loadStatus(true);
1273
+ } catch (error) {
1274
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1275
+ } finally {
1276
+ setBusy(null);
1277
+ }
1278
+ }
1279
+
1280
+ async function doResolve(conflictId, choice) {
1281
+ if (busy !== null) return;
1282
+ setBusy(`conflict:${conflictId}:${choice}`);
1283
+ setMessage(null);
1284
+ setSummary(null);
1285
+ try {
1286
+ const result = assertOk(await rpc("resolve-conflict", { id: conflictId, choice }));
1287
+ takeStatus(result);
1288
+ setMessage({ tone: "ok", text: t("conflictDone") });
1289
+ await loadStatus(true);
1290
+ } catch (error) {
1291
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1292
+ } finally {
1293
+ setBusy(null);
1294
+ }
1295
+ }
1296
+
1297
+ /* ---------------- USB portable transfer ---------------- */
1298
+
1299
+ function retry() {
1300
+ setMessage(null);
1301
+ setPhase("loading");
1302
+ void loadStatus(false);
1303
+ }
1304
+
1305
+ function portablePayload(result) {
1306
+ if (result !== null && typeof result === "object" && result.value !== null && typeof result.value === "object") {
1307
+ return result.value;
1308
+ }
1309
+ return result;
1310
+ }
1311
+
1312
+ async function runPortable(action) {
1313
+ if (portableBusy !== null) return;
1314
+ const source = portablePath.trim();
1315
+ if (source.length === 0) {
1316
+ setPortableMessage({ tone: "error", text: t("portableNoPath") });
1317
+ return;
1318
+ }
1319
+ if (action === "import") {
1320
+ const confirmed =
1321
+ typeof window === "undefined" || typeof window.confirm !== "function" ? true : window.confirm(t("portableConfirm"));
1322
+ if (!confirmed) return;
1323
+ }
1324
+ setPortableBusy(action);
1325
+ setPortableMessage(null);
1326
+ try {
1327
+ const method = action === "scan" ? "portable-scan" : action === "export" ? "portable-export" : "portable-import";
1328
+ const payload = action === "export" ? { destination: source } : { source };
1329
+ const result = assertOk(await rpc(method, payload));
1330
+ const value = portablePayload(result);
1331
+ setPortableResult(value);
1332
+ if (action === "scan") {
1333
+ const count = Array.isArray(value?.projects) ? value.projects.length : 0;
1334
+ setPortableMessage({ tone: count > 0 ? "ok" : "error", text: count > 0 ? formatTemplate(t("portableFound"), { count }) : t("portableEmpty") });
1335
+ } else if (action === "export") {
1336
+ setPortableMessage({ tone: "ok", text: formatTemplate(t("portableExported"), { count: Number(value?.projects?.length) || 0 }) });
1337
+ } else {
1338
+ setPortableMessage({ tone: "ok", text: formatTemplate(t("portableImported"), { count: Number(value?.imported) || 0 }) });
1339
+ takeStatus(result);
1340
+ await loadStatus(true);
1341
+ }
1342
+ } catch (error) {
1343
+ setPortableMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1344
+ } finally {
1345
+ setPortableBusy(null);
1346
+ }
1347
+ }
1348
+
1349
+ /* ---------------- history and restore ---------------- */
1350
+
1351
+ async function openHistory(slug) {
1352
+ if (busy !== null) return;
1353
+ setHistoryFor(slug);
1354
+ setHistoryItems(null);
1355
+ setHistoryLoading(true);
1356
+ setMessage(null);
1357
+ try {
1358
+ const result = assertOk(await rpc("history", { slug }));
1359
+ setHistoryItems(historyFrom(result));
1360
+ } catch (error) {
1361
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1362
+ setHistoryFor(null);
1363
+ } finally {
1364
+ setHistoryLoading(false);
1365
+ }
1366
+ }
1367
+
1368
+ function closeHistory() {
1369
+ setHistoryFor(null);
1370
+ setHistoryItems(null);
1371
+ setHistoryLoading(false);
1372
+ }
1373
+
1374
+ async function doRestore(slug, commit) {
1375
+ if (busy !== null) return;
1376
+ const confirmed =
1377
+ typeof window === "undefined" || typeof window.confirm !== "function" ? true : window.confirm(t("historyConfirm"));
1378
+ if (!confirmed) return;
1379
+ setBusy(`restore:${slug}`);
1380
+ setMessage(null);
1381
+ setSummary(null);
1382
+ try {
1383
+ const result = assertOk(await rpc("restore-version", { slug, commit }));
1384
+ takeStatus(result);
1385
+ setMessage({ tone: "ok", text: t("historyDone") });
1386
+ closeHistory();
1387
+ await loadStatus(true);
1388
+ } catch (error) {
1389
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
1390
+ } finally {
1391
+ setBusy(null);
1392
+ }
1393
+ }
1394
+
1395
+ /* ---------------- derived view state ---------------- */
1396
+
1397
+ const configured = status !== null && typeof status === "object" && status.configured === true;
1398
+ const github = githubFrom(status);
1399
+ const conflicts = status !== null && status !== undefined && Array.isArray(status.conflicts) ? status.conflicts : [];
1400
+ const projects = status !== null && status !== undefined && Array.isArray(status.projects) ? status.projects : [];
1401
+ const pending = status !== null && typeof status === "object" && status.pending !== null && typeof status.pending === "object" ? status.pending : {};
1402
+ const localChanges = Number(pending.localChanges) || 0;
1403
+ const unpushed = pending.unpushed === true;
1404
+ const conflictCount = conflicts.length;
1405
+ const repoVisibilityRaw = visibilityFrom(status);
1406
+ const repoVisibility = configured === true ? repoVisibilityRaw : "unknown";
1407
+ const publicRepo = repoVisibility === "public";
1408
+ const gitAvailable = status === null || typeof status !== "object" ? true : status.gitAvailable !== false;
1409
+ const lastSyncText = status !== null && typeof status === "object" ? relativeTime(status.lastSyncAt, lang) : null;
1410
+ const repoUrl = status !== null && typeof status === "object" && typeof status.repoUrl === "string" ? status.repoUrl : "";
1411
+ const branch = status !== null && typeof status === "object" && typeof status.branch === "string" && status.branch.length > 0 ? status.branch : "main";
1412
+ const repoName = status !== null && typeof status === "object" && typeof status.repo === "string" ? status.repo : "";
1413
+
1414
+ let stateTone;
1415
+ let stateText;
1416
+ if (conflictCount > 0) {
1417
+ stateTone = "error";
1418
+ stateText = pluralWord(conflictCount, "conflictOne", "conflictFew", "conflictMany");
1419
+ } else if (localChanges > 0 || unpushed) {
1420
+ stateTone = "warn";
1421
+ stateText = t("stateLocalChanges");
1422
+ } else {
1423
+ stateTone = "ok";
1424
+ stateText = t("stateSynced");
1425
+ }
1426
+
1427
+ let sectionCounter = 0;
1428
+ const nextSectionNumber = () => String((sectionCounter += 1)).padStart(2, "0");
1429
+ const numberMain = nextSectionNumber();
1430
+ const numberHow = nextSectionNumber();
1431
+ const numberSecurity = nextSectionNumber();
1432
+ const numberConflicts = conflictCount > 0 ? nextSectionNumber() : null;
1433
+ const numberProjects = nextSectionNumber();
1434
+ const numberHistory = nextSectionNumber();
1435
+ const numberPortable = nextSectionNumber();
1436
+
1437
+ const head = h(
1438
+ "div",
1439
+ { className: "dmb-head" },
1440
+ h("div", { className: "dmb-kicker" }, "DeepSeek Harness"),
1441
+ h("div", { className: "dmb-title" }, t("title")),
1442
+ h("div", { className: "dmb-sub" }, t("subtitle")),
1443
+ );
1444
+
1445
+ const banners = [];
1446
+ if (status !== null && typeof status === "object" && typeof status.lastError === "string" && status.lastError.length > 0) {
1447
+ banners.push(h(Banner, { key: "last-error", tone: "error" }, `${t("syncErrorLabel")}: ${status.lastError}`));
1448
+ }
1449
+ if (message !== null && !(phase === "error" && status === null)) {
1450
+ banners.push(h(Banner, { key: "message", tone: message.tone ?? "ok" }, message.text));
1451
+ }
1452
+ const bannersBlock = banners.length > 0 ? h("div", { className: "dmb-banners" }, banners) : null;
1453
+
1454
+ const loadingCard = h(
1455
+ "section",
1456
+ { className: "dmb-card", "aria-busy": "true" },
1457
+ h("div", { className: "dmb-loading" }, h(Spinner), h("span", null, t("loading"))),
1458
+ );
1459
+
1460
+ const errorCard = h(
1461
+ "section",
1462
+ { className: "dmb-card" },
1463
+ h(Banner, { tone: "error" }, message !== null ? message.text : t("loadError")),
1464
+ h("div", { className: "dmb-actions" }, h("button", { type: "button", className: "dmb-btn", onClick: retry }, h(Icon, { name: "sync" }), t("retry"))),
1465
+ );
1466
+
1467
+ /* ---------------- setup cards ---------------- */
1468
+
1469
+ const ghCommand = () => h("code", { className: "dmb-code" }, "gh auth login");
1470
+
1471
+ const ghMissingCard = h(
1472
+ "section",
1473
+ { className: "dmb-card", "aria-label": t("secRepo") },
1474
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("ghMissingTitle"), sub: t("ghMissingText") }),
1475
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
1476
+ h(
1477
+ "ol",
1478
+ { className: "dmb-steps" },
1479
+ h(
1480
+ "li",
1481
+ { className: "dmb-step", key: "install" },
1482
+ h("span", { className: "dmb-step-num" }, "1"),
1483
+ h(
1484
+ "div",
1485
+ { className: "dmb-step-body" },
1486
+ h("div", { className: "dmb-step-title" }, t("ghInstallStep")),
1487
+ h("div", { className: "dmb-step-text" }, t("ghInstallHint")),
1488
+ ),
1489
+ ),
1490
+ h(
1491
+ "li",
1492
+ { className: "dmb-step", key: "login" },
1493
+ h("span", { className: "dmb-step-num" }, "2"),
1494
+ h(
1495
+ "div",
1496
+ { className: "dmb-step-body" },
1497
+ h("div", { className: "dmb-step-title" }, t("ghLoginStep")),
1498
+ ghCommand(),
1499
+ h("div", { className: "dmb-step-text" }, t("ghLoginHint")),
1500
+ ),
1501
+ ),
1502
+ ),
1503
+ h(
1504
+ "div",
1505
+ { className: "dmb-actions" },
1506
+ h(
1507
+ "button",
1508
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doCheck() },
1509
+ busy === "check" ? h(Spinner) : h(Icon, { name: "terminal" }),
1510
+ busy === "check" ? t("checking") : t("checkAgain"),
1511
+ ),
1512
+ ),
1513
+ );
1514
+
1515
+ const ghLoginCard = h(
1516
+ "section",
1517
+ { className: "dmb-card", "aria-label": t("secRepo") },
1518
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("ghLoginTitle"), sub: t("ghLoginText") }),
1519
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
1520
+ h("div", { className: "dmb-step-title" }, t("ghLoginStep")),
1521
+ ghCommand(),
1522
+ h("div", { className: "dmb-hint" }, t("ghLoginHint")),
1523
+ h(
1524
+ "div",
1525
+ { className: "dmb-actions" },
1526
+ h(
1527
+ "button",
1528
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doCheck() },
1529
+ busy === "check" ? h(Spinner) : h(Icon, { name: "terminal" }),
1530
+ busy === "check" ? t("checking") : t("checkAgain"),
1531
+ ),
1532
+ ),
1533
+ );
1534
+
1535
+ const repoSetupCard = h(
1536
+ "section",
1537
+ { className: "dmb-card", "aria-label": t("secRepo") },
1538
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("repoSetupTitle"), sub: t("repoSetupText") }),
1539
+ h(
1540
+ "div",
1541
+ { className: "dmb-account" },
1542
+ h(Icon, { name: "user", size: 15 }),
1543
+ h("span", null, t("ghLoggedIn")),
1544
+ h("b", null, github.login ?? "—"),
1545
+ ),
1546
+ h("div", { className: "dmb-note" }, h(Icon, { name: "shield", size: 15 }), h("span", null, t("accountHint"))),
1547
+ h(
1548
+ "div",
1549
+ { className: "dmb-field" },
1550
+ h("label", { className: "dmb-label", htmlFor: "dmb-repo" }, t("repoLabel")),
1551
+ h("input", {
1552
+ id: "dmb-repo",
1553
+ className: "dmb-input dmb-input--mono",
1554
+ type: "text",
1555
+ value: repoInput,
1556
+ placeholder: t("repoPlaceholder"),
1557
+ spellCheck: false,
1558
+ autoComplete: "off",
1559
+ disabled: busy !== null,
1560
+ onChange: (event) => {
1561
+ repoEditedRef.current = true;
1562
+ setRepoInput(event.target.value);
1563
+ },
1564
+ onKeyDown: (event) => {
1565
+ if (event.key === "Enter") {
1566
+ event.preventDefault();
1567
+ void doConnect("create");
1568
+ }
1569
+ },
1570
+ }),
1571
+ h("div", { className: "dmb-hint" }, t("repoFieldHint")),
1572
+ ),
1573
+ h(
1574
+ "div",
1575
+ { className: "dmb-actions" },
1576
+ h(
1577
+ "button",
1578
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doConnect("create") },
1579
+ busy === "create" ? h(Spinner) : h(Icon, { name: "plus" }),
1580
+ busy === "create" ? t("creatingRepo") : t("createRepo"),
1581
+ ),
1582
+ h(
1583
+ "button",
1584
+ { type: "button", className: "dmb-btn", disabled: busy !== null, onClick: () => void doConnect("configure") },
1585
+ busy === "configure" ? h(Spinner) : h(Icon, { name: "repo" }),
1586
+ busy === "configure" ? t("connectingRepo") : t("connectExisting"),
1587
+ ),
1588
+ ),
1589
+ h("div", { className: "dmb-note" }, h(Icon, { name: "lock", size: 15 }), h("span", null, t("privateOnlyNote"))),
1590
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
1591
+ );
1592
+
1593
+ /* ---------------- connected status card ---------------- */
1594
+
1595
+ const visibilityChip =
1596
+ repoVisibility === "private"
1597
+ ? h(Chip, { tone: "ok", icon: "lock" }, t("visPrivate"))
1598
+ : repoVisibility === "public"
1599
+ ? h(Chip, { tone: "error", icon: "alert" }, t("visPublic"))
1600
+ : h(Chip, { tone: "muted", icon: "eyeOff" }, t("visUnknown"));
1601
+
1602
+ const stateBlock = h(
1603
+ "div",
1604
+ { className: "dmb-state", "data-tone": stateTone },
1605
+ h("span", { className: "dmb-state-icon" }, h(Icon, { name: stateTone === "ok" ? "check" : "alert", size: 18 })),
1606
+ h(
1607
+ "div",
1608
+ { className: "dmb-state-body" },
1609
+ h("div", { className: "dmb-state-title" }, stateText),
1610
+ h(
1611
+ "div",
1612
+ { className: "dmb-state-meta" },
1613
+ visibilityChip,
1614
+ github.login !== null ? h("span", null, `${t("ghLoggedIn")} `, h("b", null, github.login)) : null,
1615
+ h("span", null, `${t("lastSync")}: `, h("span", null, lastSyncText ?? t("never"))),
1616
+ localChanges > 0 ? h(Chip, { tone: "warn" }, `${t("pendingLocal")}: ${localChanges}`) : null,
1617
+ unpushed === true ? h(Chip, { tone: "warn" }, t("pendingUnpushed")) : null,
1618
+ ),
1619
+ ),
1620
+ );
1621
+
1622
+ const repoLine = h(
1623
+ "div",
1624
+ { className: "dmb-repo-line" },
1625
+ repoUrl.length > 0
1626
+ ? h("a", { className: "dmb-link", href: repoUrl, target: "_blank", rel: "noopener noreferrer" }, repoName.length > 0 ? repoName : repoUrl)
1627
+ : h("span", { className: "dmb-mono" }, repoName.length > 0 ? repoName : "—"),
1628
+ h("span", { className: "dmb-sep", "aria-hidden": "true" }, "·"),
1629
+ h("span", null, `${t("branch")}: `, h("span", { className: "dmb-mono" }, branch)),
1630
+ );
1631
+
1632
+ const summaryBlock =
1633
+ summary !== null
1634
+ ? h(
1635
+ "div",
1636
+ { className: "dmb-preview" },
1637
+ h(
1638
+ "div",
1639
+ { className: "dmb-grid3" },
1640
+ h(KV, { label: t("summaryProjects") }, String(summary.projects ?? 0)),
1641
+ h(KV, { label: t("summaryFiles") }, String(summary.files ?? 0)),
1642
+ h(KV, { label: t("summaryConflicts") }, String(summary.conflicts ?? 0)),
1643
+ ),
1644
+ h("div", { className: "dmb-hint" }, summary.pushed === true ? t("summaryPushed") : t("summaryLocalOnly")),
1645
+ )
1646
+ : null;
1647
+
1648
+ const optionsPanel =
1649
+ optionsOpen === true
1650
+ ? h(
1651
+ "div",
1652
+ { className: "dmb-options" },
1653
+ h("div", { className: "dmb-card-title" }, t("optionsTitle")),
1654
+ h(
1655
+ "label",
1656
+ { className: "dmb-check" },
1657
+ h("input", {
1658
+ type: "checkbox",
1659
+ checked: draft.autoSync === true,
1660
+ disabled: busy !== null,
1661
+ onChange: (event) => setDraft((current) => ({ ...current, autoSync: event.target.checked })),
1662
+ }),
1663
+ h(
1664
+ "span",
1665
+ { className: "dmb-check-text" },
1666
+ h("span", { className: "dmb-check-title" }, t("autoSync")),
1667
+ h("span", { className: "dmb-hint" }, t("autoSyncHint")),
1668
+ ),
1669
+ ),
1670
+ h(
1671
+ "div",
1672
+ { className: "dmb-field" },
1673
+ h("label", { className: "dmb-label", htmlFor: "dmb-interval" }, t("intervalLabel")),
1674
+ h(
1675
+ "select",
1676
+ {
1677
+ id: "dmb-interval",
1678
+ className: "dmb-select",
1679
+ value: draft.intervalSeconds,
1680
+ disabled: busy !== null,
1681
+ onChange: (event) => setDraft((current) => ({ ...current, intervalSeconds: event.target.value })),
1682
+ },
1683
+ ALLOWED_INTERVALS.map((seconds) => h("option", { key: String(seconds), value: String(seconds) }, `${seconds} ${t("secondsShort")}`)),
1684
+ ),
1685
+ ),
1686
+ h(
1687
+ "div",
1688
+ { className: "dmb-field" },
1689
+ h("label", { className: "dmb-label", htmlFor: "dmb-exclude" }, t("excludeLabel")),
1690
+ h("input", {
1691
+ id: "dmb-exclude",
1692
+ className: "dmb-input dmb-input--mono",
1693
+ type: "text",
1694
+ value: draft.excludeProjects,
1695
+ placeholder: t("excludePlaceholder"),
1696
+ spellCheck: false,
1697
+ autoComplete: "off",
1698
+ disabled: busy !== null,
1699
+ onChange: (event) => setDraft((current) => ({ ...current, excludeProjects: event.target.value })),
1700
+ }),
1701
+ h("div", { className: "dmb-hint" }, t("excludeHint")),
1702
+ ),
1703
+ h(
1704
+ "div",
1705
+ { className: "dmb-actions" },
1706
+ h(
1707
+ "button",
1708
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doSaveOptions() },
1709
+ busy === "options" ? h(Spinner) : h(Icon, { name: "check" }),
1710
+ busy === "options" ? t("saving") : t("saveOptions"),
1711
+ ),
1712
+ ),
1713
+ )
1714
+ : null;
1715
+
1716
+ const statusCard = h(
1717
+ "section",
1718
+ { className: "dmb-card", "aria-label": t("secSync") },
1719
+ h(
1720
+ "div",
1721
+ { className: "dmb-card-head" },
1722
+ h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberMain, label: t("secSync") })),
1723
+ h(Chip, { tone: status?.enabled === true ? "ok" : "muted", icon: status?.enabled === true ? "check" : "power" }, status?.enabled === true ? t("enabledBadge") : t("disabledBadge")),
1724
+ ),
1725
+ stateBlock,
1726
+ repoLine,
1727
+ publicRepo === true ? h(Banner, { tone: "error" }, t("publicRepoWarn")) : null,
1728
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
1729
+ github.authenticated !== true ? h(Banner, { tone: "warn" }, t("ghNotAuthorizedWarn")) : null,
1730
+ h(
1731
+ "div",
1732
+ { className: "dmb-actions" },
1733
+ h(
1734
+ "button",
1735
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null || publicRepo === true, onClick: () => void doSync() },
1736
+ busy === "sync" ? h(Spinner) : h(Icon, { name: "sync" }),
1737
+ busy === "sync" ? t("syncing") : t("syncNow"),
1738
+ ),
1739
+ h(
1740
+ "button",
1741
+ { type: "button", className: "dmb-btn", disabled: busy !== null, onClick: () => void doToggle() },
1742
+ busy === "toggle" ? h(Spinner) : h(Icon, { name: "power" }),
1743
+ status?.enabled === true ? t("disable") : t("enable"),
1744
+ ),
1745
+ h(
1746
+ "button",
1747
+ { type: "button", className: "dmb-btn", disabled: busy !== null || repoUrl.length === 0, onClick: openRepo },
1748
+ h(Icon, { name: "external" }),
1749
+ t("openRepo"),
1750
+ ),
1751
+ h(
1752
+ "button",
1753
+ { type: "button", className: "dmb-btn", "aria-expanded": howOpen === true, onClick: toggleHow },
1754
+ h(Icon, { name: "help" }),
1755
+ t("howButton"),
1756
+ ),
1757
+ h(
1758
+ "button",
1759
+ { type: "button", className: "dmb-btn", "aria-expanded": optionsOpen === true, onClick: toggleOptions },
1760
+ h(Icon, { name: "gear" }),
1761
+ optionsOpen === true ? t("optionsHide") : t("optionsShow"),
1762
+ ),
1763
+ ),
1764
+ summaryBlock,
1765
+ optionsPanel !== null ? h("div", { className: "dmb-divider" }) : null,
1766
+ optionsPanel,
1767
+ );
1768
+
1769
+ /* ---------------- how it works ---------------- */
1770
+
1771
+ const howCard = h(
1772
+ "section",
1773
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secHow") },
1774
+ h(
1775
+ "div",
1776
+ { className: "dmb-card-head" },
1777
+ h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberHow, label: t("secHow") })),
1778
+ h(
1779
+ "button",
1780
+ { type: "button", className: "dmb-btn dmb-btn--ghost dmb-btn--small", "aria-expanded": howOpen === true, onClick: toggleHow },
1781
+ h(Icon, { name: howOpen === true ? "chevronUp" : "chevronDown" }),
1782
+ howOpen === true ? t("howHide") : t("howShow"),
1783
+ ),
1784
+ ),
1785
+ howOpen === true
1786
+ ? h(
1787
+ "ol",
1788
+ { className: "dmb-steps" },
1789
+ h(
1790
+ "li",
1791
+ { className: "dmb-step", key: "how1" },
1792
+ h("span", { className: "dmb-step-num" }, "01"),
1793
+ h(
1794
+ "div",
1795
+ { className: "dmb-step-body" },
1796
+ h("div", { className: "dmb-step-title" }, t("how1Title")),
1797
+ h("div", { className: "dmb-step-text" }, t("how1Text")),
1798
+ ),
1799
+ ),
1800
+ h(
1801
+ "li",
1802
+ { className: "dmb-step", key: "how2" },
1803
+ h("span", { className: "dmb-step-num" }, "02"),
1804
+ h(
1805
+ "div",
1806
+ { className: "dmb-step-body" },
1807
+ h("div", { className: "dmb-step-title" }, t("how2Title")),
1808
+ h("div", { className: "dmb-step-text" }, t("how2Text")),
1809
+ ),
1810
+ ),
1811
+ h(
1812
+ "li",
1813
+ { className: "dmb-step", key: "how3" },
1814
+ h("span", { className: "dmb-step-num" }, "03"),
1815
+ h(
1816
+ "div",
1817
+ { className: "dmb-step-body" },
1818
+ h("div", { className: "dmb-step-title" }, t("how3Title")),
1819
+ h("div", { className: "dmb-step-text" }, t("how3Text")),
1820
+ ),
1821
+ ),
1822
+ )
1823
+ : null,
1824
+ );
1825
+
1826
+ /* ---------------- security ---------------- */
1827
+
1828
+ const securityCard = h(
1829
+ "section",
1830
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secSecurity") },
1831
+ h("div", { className: "dmb-card-head" }, h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberSecurity, label: t("secSecurity") }))),
1832
+ h(
1833
+ "ul",
1834
+ { className: "dmb-security" },
1835
+ h(
1836
+ "li",
1837
+ { key: "sec1" },
1838
+ h(Icon, { name: "lock", size: 15 }),
1839
+ h(
1840
+ "div",
1841
+ null,
1842
+ h("div", { className: "dmb-security-title" }, t("sec1Title")),
1843
+ h("div", { className: "dmb-security-text" }, t("sec1Text")),
1844
+ ),
1845
+ ),
1846
+ h(
1847
+ "li",
1848
+ { key: "sec2" },
1849
+ h(Icon, { name: "eyeOff", size: 15 }),
1850
+ h(
1851
+ "div",
1852
+ null,
1853
+ h("div", { className: "dmb-security-title" }, t("sec2Title")),
1854
+ h("div", { className: "dmb-security-text" }, t("sec2Text")),
1855
+ ),
1856
+ ),
1857
+ h(
1858
+ "li",
1859
+ { key: "sec3" },
1860
+ h(Icon, { name: "file", size: 15 }),
1861
+ h(
1862
+ "div",
1863
+ null,
1864
+ h("div", { className: "dmb-security-title" }, t("sec3Title")),
1865
+ h("div", { className: "dmb-security-text" }, t("sec3Text")),
1866
+ ),
1867
+ ),
1868
+ h(
1869
+ "li",
1870
+ { key: "sec4" },
1871
+ h(Icon, { name: "history", size: 15 }),
1872
+ h(
1873
+ "div",
1874
+ null,
1875
+ h("div", { className: "dmb-security-title" }, t("sec4Title")),
1876
+ h("div", { className: "dmb-security-text" }, t("sec4Text")),
1877
+ ),
1878
+ ),
1879
+ ),
1880
+ );
1881
+
1882
+ /* ---------------- conflicts ---------------- */
1883
+
1884
+ function renderDiffPane(text, otherText, changedClass) {
1885
+ const rows = diffRows(text, otherText);
1886
+ return h(
1887
+ "pre",
1888
+ { className: "dmb-pre dmb-diff" },
1889
+ rows.map((row, index) =>
1890
+ h(
1891
+ "span",
1892
+ { key: index, className: `dmb-diff-line${row.changed ? ` ${changedClass}` : ""}` },
1893
+ row.text.length > 0 ? row.text : " ",
1894
+ ),
1895
+ ),
1896
+ );
1897
+ }
1898
+
1899
+ function renderConflict(conflict, index) {
1900
+ const id = String(conflict?.id ?? index);
1901
+ const path = typeof conflict?.path === "string" ? conflict.path : "";
1902
+ const projectName = conflict?.projectName || conflict?.projectSlug || id;
1903
+ const localPreview = typeof conflict?.localPreview === "string" && conflict.localPreview.length > 0 ? conflict.localPreview : t("previewEmpty");
1904
+ const remotePreview = typeof conflict?.remotePreview === "string" && conflict.remotePreview.length > 0 ? conflict.remotePreview : t("previewEmpty");
1905
+ const found = formatTime(conflict?.createdAt);
1906
+ const isWorking = (choice) => busy === `conflict:${id}:${choice}`;
1907
+ const disabled = busy !== null;
1908
+ return h(
1909
+ "div",
1910
+ { className: "dmb-conflict", key: id },
1911
+ h(
1912
+ "div",
1913
+ { className: "dmb-conflict-head" },
1914
+ h("div", { className: "dmb-conflict-title" }, projectName),
1915
+ found !== null ? h("span", { className: "dmb-hint" }, `${t("conflictFound")}: ${found}`) : null,
1916
+ ),
1917
+ path.length > 0 ? h("div", { className: "dmb-mono dmb-path" }, path) : null,
1918
+ h(
1919
+ "div",
1920
+ { className: "dmb-grid2" },
1921
+ h(
1922
+ "div",
1923
+ { className: "dmb-preview" },
1924
+ h("div", { className: "dmb-label" }, t("conflictMine")),
1925
+ renderDiffPane(localPreview, remotePreview, "dmb-diff-del"),
1926
+ ),
1927
+ h(
1928
+ "div",
1929
+ { className: "dmb-preview" },
1930
+ h("div", { className: "dmb-label" }, t("conflictRemote")),
1931
+ renderDiffPane(remotePreview, localPreview, "dmb-diff-add"),
1932
+ ),
1933
+ ),
1934
+ h("div", { className: "dmb-diff-legend" }, t("diffLegend")),
1935
+ h(
1936
+ "div",
1937
+ { className: "dmb-actions" },
1938
+ h(
1939
+ "button",
1940
+ { type: "button", className: "dmb-btn", disabled, onClick: () => void doResolve(id, "local") },
1941
+ isWorking("local") ? h(Spinner) : null,
1942
+ isWorking("local") ? t("conflictWorking") : t("conflictKeepMine"),
1943
+ ),
1944
+ h(
1945
+ "button",
1946
+ { type: "button", className: "dmb-btn", "data-tone": "caution", disabled, onClick: () => void doResolve(id, "remote") },
1947
+ isWorking("remote") ? h(Spinner) : null,
1948
+ isWorking("remote") ? t("conflictWorking") : t("conflictTakeRemote"),
1949
+ ),
1950
+ h(
1951
+ "button",
1952
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled, onClick: () => void doResolve(id, "both") },
1953
+ isWorking("both") ? h(Spinner) : null,
1954
+ isWorking("both") ? t("conflictWorking") : t("conflictMerge"),
1955
+ ),
1956
+ ),
1957
+ h(
1958
+ "div",
1959
+ { className: "dmb-hints" },
1960
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "chevronUp", size: 12 }), h("span", null, t("hintKeepMine"))),
1961
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "chevronDown", size: 12 }), h("span", null, t("hintTakeRemote"))),
1962
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "check", size: 12 }), h("span", null, t("hintMerge"))),
1963
+ ),
1964
+ );
1965
+ }
1966
+
1967
+ const conflictsCard =
1968
+ conflictCount > 0
1969
+ ? h(
1970
+ "section",
1971
+ { className: "dmb-card", "aria-label": t("secConflicts") },
1972
+ h(CardHead, {
1973
+ number: numberConflicts,
1974
+ label: t("secConflicts"),
1975
+ title: t("conflictsTitle"),
1976
+ sub: t("conflictsSubtitle"),
1977
+ right: h(Chip, { tone: "error" }, String(conflictCount)),
1978
+ }),
1979
+ conflicts.map(renderConflict),
1980
+ )
1981
+ : null;
1982
+
1983
+ /* ---------------- projects ---------------- */
1984
+
1985
+ const projectsList =
1986
+ projects.length === 0
1987
+ ? h("div", { className: "dmb-empty" }, t("projectsEmpty"))
1988
+ : h(
1989
+ "div",
1990
+ { className: "dmb-list" },
1991
+ projects.map((project, index) =>
1992
+ h(
1993
+ "div",
1994
+ { className: "dmb-row", key: String(project?.slug ?? index) },
1995
+ h(
1996
+ "div",
1997
+ { className: "dmb-row-text" },
1998
+ h("div", { className: "dmb-row-title" }, project?.name || project?.slug || "—"),
1999
+ typeof project?.slug === "string" && project.slug.length > 0 ? h("div", { className: "dmb-row-desc dmb-mono" }, project.slug) : null,
2000
+ ),
2001
+ h(Chip, { tone: projectStatusTone(project?.status) }, t(projectStatusKey(project?.status))),
2002
+ h("div", { className: "dmb-row-time" }, relativeTime(project?.lastSyncAt, lang) ?? "—"),
2003
+ h(
2004
+ "button",
2005
+ {
2006
+ type: "button",
2007
+ className: "dmb-btn dmb-btn--sm",
2008
+ disabled: busy !== null,
2009
+ onClick: () => void openHistory(String(project?.slug ?? "")),
2010
+ },
2011
+ t("historyOpen"),
2012
+ ),
2013
+ ),
2014
+ ),
2015
+ );
2016
+
2017
+ const projectsCard = h(
2018
+ "section",
2019
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secProjects") },
2020
+ h(CardHead, {
2021
+ number: numberProjects,
2022
+ label: t("secProjects"),
2023
+ right: h(Chip, { tone: "muted" }, String(projects.length)),
2024
+ }),
2025
+ projectsList,
2026
+ );
2027
+
2028
+ /* ---------------- history ---------------- */
2029
+
2030
+ const historyProject =
2031
+ historyFor === null ? null : projects.find((project) => String(project?.slug ?? "") === historyFor) ?? null;
2032
+ const historyCard =
2033
+ historyFor === null
2034
+ ? null
2035
+ : h(
2036
+ "section",
2037
+ { className: "dmb-card", "aria-label": t("historyTitle") },
2038
+ h(CardHead, {
2039
+ number: numberHistory,
2040
+ label: t("historyTitle"),
2041
+ title: historyProject?.name || historyFor,
2042
+ sub: t("historySubtitle"),
2043
+ right: h(
2044
+ "button",
2045
+ { type: "button", className: "dmb-btn dmb-btn--sm", disabled: busy !== null, onClick: closeHistory },
2046
+ t("historyClose"),
2047
+ ),
2048
+ }),
2049
+ historyLoading
2050
+ ? h("div", { className: "dmb-empty" }, t("historyLoading"))
2051
+ : historyItems === null || historyItems.length === 0
2052
+ ? h("div", { className: "dmb-empty" }, t("historyEmpty"))
2053
+ : h(
2054
+ "div",
2055
+ { className: "dmb-list" },
2056
+ historyItems.map((item, index) => {
2057
+ const sha = String(item?.sha ?? "");
2058
+ return h(
2059
+ "div",
2060
+ { className: "dmb-row", key: sha || String(index) },
2061
+ h(
2062
+ "div",
2063
+ { className: "dmb-row-text" },
2064
+ h("div", { className: "dmb-row-title" }, item?.subject || sha.slice(0, 8) || "—"),
2065
+ h(
2066
+ "div",
2067
+ { className: "dmb-row-desc" },
2068
+ [formatTime(item?.date) ?? item?.date ?? "", item?.author ? `${t("historyAuthor")} ${item.author}` : ""]
2069
+ .filter((part) => typeof part === "string" && part.length > 0)
2070
+ .join(" · "),
2071
+ ),
2072
+ ),
2073
+ h(
2074
+ "button",
2075
+ {
2076
+ type: "button",
2077
+ className: "dmb-btn dmb-btn--sm",
2078
+ disabled: busy !== null,
2079
+ onClick: () => void doRestore(historyFor, sha),
2080
+ },
2081
+ busy === `restore:${historyFor}` ? h(Spinner) : t("historyRestore"),
2082
+ ),
2083
+ );
2084
+ }),
2085
+ ),
2086
+ h("div", { className: "dmb-hint" }, t("historyRestoreHint")),
2087
+ );
2088
+
2089
+ /* ---------------- portable USB ---------------- */
2090
+
2091
+ const portableProjects =
2092
+ portableResult !== null && typeof portableResult === "object" && Array.isArray(portableResult.projects)
2093
+ ? portableResult.projects
2094
+ : [];
2095
+ const portableStatusKey = (value) => {
2096
+ const map = {
2097
+ new: "portableNew",
2098
+ update: "portableUpdate",
2099
+ same: "portableSame",
2100
+ imported: "portableImportedState",
2101
+ "imported-new": "portableImportedNew",
2102
+ skipped: "portableSkipped",
2103
+ };
2104
+ return map[String(value ?? "")] ?? "portableSame";
2105
+ };
2106
+ const portableStatusTone = (value) =>
2107
+ value === "same" || value === "imported" || value === "imported-new"
2108
+ ? "ok"
2109
+ : value === "update"
2110
+ ? "warn"
2111
+ : value === "new"
2112
+ ? "muted"
2113
+ : "muted";
2114
+ const portableCard =
2115
+ status === null
2116
+ ? null
2117
+ : h(
2118
+ "section",
2119
+ { className: "dmb-card dmb-usb", "aria-label": t("portableTitle") },
2120
+ h(CardHead, {
2121
+ number: numberPortable,
2122
+ label: t("portableTitle"),
2123
+ title: t("portableHeading"),
2124
+ sub: t("portableSubtitle"),
2125
+ right: h(
2126
+ "span",
2127
+ { className: "dmb-usb-badge" },
2128
+ h(Icon, { name: "usb", size: 12 }),
2129
+ t("portableOnlyMemory"),
2130
+ ),
2131
+ }),
2132
+ h(
2133
+ "div",
2134
+ { className: "dmb-usb-steps", "aria-hidden": "true" },
2135
+ [
2136
+ ["save", "portableStepSave"],
2137
+ ["scan", "portableStepScan"],
2138
+ ["check", "portableStepImport"],
2139
+ ].map(([icon, key], index) =>
2140
+ h(
2141
+ "div",
2142
+ { className: "dmb-usb-step", key },
2143
+ h("span", { className: "dmb-usb-step-icon" }, h(Icon, { name: icon, size: 13 })),
2144
+ h("span", { className: "dmb-usb-step-label" }, t(key)),
2145
+ index < 2 ? h("span", { className: "dmb-usb-step-arrow" }, "→") : null,
2146
+ ),
2147
+ ),
2148
+ ),
2149
+ h(
2150
+ "div",
2151
+ { className: "dmb-usb-field" },
2152
+ h("span", { className: "dmb-usb-field-icon", "aria-hidden": "true" }, h(Icon, { name: "usb", size: 16 })),
2153
+ h("input", {
2154
+ className: "dmb-input dmb-input--mono",
2155
+ type: "text",
2156
+ value: portablePath,
2157
+ placeholder: t("portablePlaceholder"),
2158
+ spellCheck: false,
2159
+ "aria-label": t("portablePlaceholder"),
2160
+ disabled: portableBusy !== null,
2161
+ onChange: (event) => setPortablePath(event.target.value),
2162
+ }),
2163
+ ),
2164
+ h(
2165
+ "div",
2166
+ { className: "dmb-usb-actions" },
2167
+ h(
2168
+ "button",
2169
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: portableBusy !== null, onClick: () => void runPortable("export") },
2170
+ portableBusy === "export" ? h(Spinner) : h(Icon, { name: "save", size: 14 }),
2171
+ t("portableExport"),
2172
+ ),
2173
+ h(
2174
+ "button",
2175
+ { type: "button", className: "dmb-btn", disabled: portableBusy !== null, onClick: () => void runPortable("scan") },
2176
+ portableBusy === "scan" ? h(Spinner) : h(Icon, { name: "scan", size: 14 }),
2177
+ t("portableScan"),
2178
+ ),
2179
+ h(
2180
+ "button",
2181
+ { type: "button", className: "dmb-btn", disabled: portableBusy !== null || portableProjects.length === 0, onClick: () => void runPortable("import") },
2182
+ portableBusy === "import" ? h(Spinner) : h(Icon, { name: "check", size: 14 }),
2183
+ t("portableImport"),
2184
+ ),
2185
+ ),
2186
+ portableMessage !== null ? h(Banner, { tone: portableMessage.tone ?? "ok" }, portableMessage.text) : null,
2187
+ portableProjects.length > 0
2188
+ ? h(
2189
+ "div",
2190
+ { className: "dmb-list dmb-portable-projects" },
2191
+ portableProjects.map((project, index) => {
2192
+ const fileCount = Array.isArray(project?.files) ? project.files.length : Number(project?.files) || 0;
2193
+ return h(
2194
+ "div",
2195
+ { className: "dmb-row", key: String(project?.slug ?? index) },
2196
+ h(
2197
+ "div",
2198
+ { className: "dmb-row-text" },
2199
+ h("div", { className: "dmb-row-title" }, project?.name || project?.slug || "—"),
2200
+ h("div", { className: "dmb-row-desc" }, `${project?.slug || "—"} · ${fileCount}`),
2201
+ ),
2202
+ h(Chip, { tone: portableStatusTone(project?.status) }, t(portableStatusKey(project?.status))),
2203
+ );
2204
+ }),
2205
+ )
2206
+ : null,
2207
+ h("div", { className: "dmb-portable-hint" }, t("portableHint")),
2208
+ h("div", { className: "dmb-usb-cross" }, h(Icon, { name: "repo", size: 13 }), h("span", null, t("portableCross"))),
2209
+ );
2210
+
2211
+ /* ---------------- body ---------------- */
2212
+
2213
+ let body;
2214
+ if (phase === "loading" || (status === null && phase !== "error")) body = loadingCard;
2215
+ else if (phase === "error" && status === null) body = errorCard;
2216
+ else if (status === null) body = loadingCard;
2217
+ else if (configured === true) body = statusCard;
2218
+ else if (github.available !== true) body = ghMissingCard;
2219
+ else if (github.authenticated !== true) body = ghLoginCard;
2220
+ else body = repoSetupCard;
2221
+
2222
+ const showSections = status !== null && phase === "ready";
2223
+
2224
+ return h(
2225
+ "div",
2226
+ { className: "dmb-page", "aria-busy": busy !== null ? "true" : "false" },
2227
+ head,
2228
+ bannersBlock,
2229
+ body,
2230
+ showSections ? howCard : null,
2231
+ showSections ? securityCard : null,
2232
+ showSections ? conflictsCard : null,
2233
+ showSections ? projectsCard : null,
2234
+ showSections ? historyCard : null,
2235
+ showSections ? portableCard : null,
2236
+ );
2237
+ }
2238
+
2239
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
2240
+ name: "settings.section",
2241
+ id: "memory-bridge",
2242
+ order: 35,
2243
+ label: () => t("nav"),
2244
+ locale: NS,
2245
+ inject: () => ({ call: callbacks.call }),
2246
+ }, MemorySyncSection)), "memory-bridge: settings section");
2247
+
2248
+ try {
2249
+ window.__DSH_MEMORY_BRIDGE_UI__ = {
2250
+ call: callbacks.call,
2251
+ openUrl: callbacks.openUrl,
2252
+ };
2253
+ } catch {
2254
+ /* hardened page */
2255
+ }
2256
+
2257
+ console.info("[memory-bridge] ready");
2258
+ }
2259
+
2260
+ exports.apply = apply;
2261
+ exports.inject = inject;
2262
+ return module.exports;
2263
+ },
2264
+ });