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
package/client.js CHANGED
@@ -1411,7 +1411,6 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1411
1411
 
1412
1412
  exports.apply = apply;
1413
1413
  exports.inject = inject;
1414
-
1415
1414
  return module.exports;
1416
1415
  })());
1417
1416
 
@@ -1915,7 +1914,6 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1915
1914
 
1916
1915
  exports.apply = apply;
1917
1916
  exports.inject = inject;
1918
-
1919
1917
  return module.exports;
1920
1918
  })());
1921
1919
 
@@ -1935,6 +1933,26 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1935
1933
  { from: 1, to: 4, id: "morning" },
1936
1934
  { from: 6, to: 10, id: "day" },
1937
1935
  ];
1936
+ // Official DeepSeek peak windows (01:00–04:00 and 06:00–10:00 UTC, Mon–Fri)
1937
+ // exclude Chinese public holidays. Ranges are Beijing local dates, from the
1938
+ // State Council 2026/2027 holiday notices. Weekends are off-peak by rule.
1939
+ const HOLIDAY_RANGES = [
1940
+ ["2026-01-01", "2026-01-03"],
1941
+ ["2026-02-15", "2026-02-23"],
1942
+ ["2026-04-04", "2026-04-06"],
1943
+ ["2026-05-01", "2026-05-05"],
1944
+ ["2026-06-19", "2026-06-21"],
1945
+ ["2026-09-25", "2026-09-27"],
1946
+ ["2026-10-01", "2026-10-07"],
1947
+ ["2027-01-01", "2027-01-03"],
1948
+ ["2027-02-05", "2027-02-13"],
1949
+ ["2027-04-03", "2027-04-05"],
1950
+ ["2027-05-01", "2027-05-05"],
1951
+ ["2027-06-09", "2027-06-09"],
1952
+ ["2027-09-15", "2027-09-15"],
1953
+ ["2027-10-01", "2027-10-07"],
1954
+ ];
1955
+ const SCHEDULE_VERIFIED = "2026-11-09";
1938
1956
  const TICK_MS = 1000;
1939
1957
  const PEAK_LEAD_MS = 5 * 60 * 1000;
1940
1958
  const DISMISS_KEY = "dsh-peak-guard:dismissed-window-end";
@@ -1946,7 +1964,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1946
1964
  const EN = {
1947
1965
  badgeLabel: "Peak ×2",
1948
1966
  badgeTitle: "Peak pricing is active now: tokens cost 2×. {wait} left in this window.",
1949
- soonBadgeLabel: "Peak in {wait}",
1967
+ soonBadgeLabel: "Peak in {wait}",
1950
1968
  nowLabel: "Now",
1951
1969
  kicker: "Peak pricing",
1952
1970
  soonKicker: "Peak coming",
@@ -1972,7 +1990,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1972
1990
  scheduleChipLabel: "Peak windows",
1973
1991
  scheduleUtc: "UTC: {windows}",
1974
1992
  scheduleLocal: "Your time ({zone}): {windows}",
1975
- holidayNote: "Chinese public holidays are off-peak too; only regular weekdays are shown.",
1993
+ holidayNote: "Peak windows skip weekends and Chinese public holidays (2026–2027 calendar is embedded); all other hours are off-peak.",
1976
1994
  dismiss: "Hide for this peak",
1977
1995
  soonDismiss: "Hide until the peak starts",
1978
1996
  details: "Schedule",
@@ -1980,11 +1998,21 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1980
1998
  waitHours: "{hours} h {minutes} min",
1981
1999
  waitMinutes: "{minutes} min",
1982
2000
  waitSeconds: "{seconds} s",
2001
+ toastKicker: "Peak pricing",
2002
+ toastTitle: "Tokens cost double right now",
2003
+ toastSoonTitle: "Peak pricing starts in {wait}",
2004
+ toastMeta: "{wait} left · {start}–{end} {zone}",
2005
+ toastSoonMeta: "starts in {wait} · {start}–{end} {zone}",
2006
+ toastSchedule: "Schedule",
2007
+ toastHide: "Hide",
2008
+ toastDetails: "Details",
2009
+ toastVerified: "Peak schedule verified {date}",
2010
+ toastAria: "Peak pricing warning",
1983
2011
  };
1984
2012
  const RU = {
1985
2013
  badgeLabel: "Пик ×2",
1986
2014
  badgeTitle: "Сейчас пиковый тариф: токены ×2. До конца окна осталось {wait}.",
1987
- soonBadgeLabel: "Пик через {wait}",
2015
+ soonBadgeLabel: "Пик через {wait}",
1988
2016
  nowLabel: "Сейчас",
1989
2017
  kicker: "Пиковый тариф",
1990
2018
  soonKicker: "Скоро пик",
@@ -2010,7 +2038,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2010
2038
  scheduleChipLabel: "Окна пика",
2011
2039
  scheduleUtc: "UTC: {windows}",
2012
2040
  scheduleLocal: "По вашему времени ({zone}): {windows}",
2013
- holidayNote: "Государственные праздники Китая тоже считаются off-peak — выше только обычные будни.",
2041
+ holidayNote: "Пиковые окна не действуют по выходным и в государственные праздники Китая (календарь 2026–2027 встроен); всё остальное время — off-peak.",
2014
2042
  dismiss: "Скрыть на этот пик",
2015
2043
  soonDismiss: "Скрыть до начала пика",
2016
2044
  details: "Расписание",
@@ -2018,11 +2046,21 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2018
2046
  waitHours: "{hours} ч {minutes} мин",
2019
2047
  waitMinutes: "{minutes} мин",
2020
2048
  waitSeconds: "{seconds} с",
2049
+ toastKicker: "Пиковый тариф",
2050
+ toastTitle: "Токены сейчас стоят вдвое дороже",
2051
+ toastSoonTitle: "Пиковый тариф через {wait}",
2052
+ toastMeta: "осталось {wait} · {start}–{end} {zone}",
2053
+ toastSoonMeta: "начнётся через {wait} · {start}–{end} {zone}",
2054
+ toastSchedule: "Расписание",
2055
+ toastHide: "Скрыть",
2056
+ toastDetails: "Подробнее",
2057
+ toastVerified: "Расписание проверено {date}",
2058
+ toastAria: "Предупреждение о пиковом тарифе",
2021
2059
  };
2022
2060
  const ZH = {
2023
2061
  badgeLabel: "高峰 ×2",
2024
2062
  badgeTitle: "当前为高峰时段:Token 价格 ×2。本时段还剩 {wait}。",
2025
- soonBadgeLabel: "高峰还剩 {wait}",
2063
+ soonBadgeLabel: "高峰还剩 {wait}",
2026
2064
  nowLabel: "现在",
2027
2065
  kicker: "高峰价格",
2028
2066
  soonKicker: "高峰将至",
@@ -2048,7 +2086,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2048
2086
  scheduleChipLabel: "高峰时段",
2049
2087
  scheduleUtc: "UTC:{windows}",
2050
2088
  scheduleLocal: "本地时间({zone}):{windows}",
2051
- holidayNote: "中国法定节假日同样按错峰计费;上表仅适用于普通工作日。",
2089
+ holidayNote: "高峰时段不包含周末与中国法定节假日(已内置 2026–2027 日历);其余时间均为错峰价。",
2052
2090
  dismiss: "本次高峰不再提示",
2053
2091
  soonDismiss: "高峰开始前不再提示",
2054
2092
  details: "时间表",
@@ -2056,6 +2094,16 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2056
2094
  waitHours: "{hours} 小时 {minutes} 分钟",
2057
2095
  waitMinutes: "{minutes} 分钟",
2058
2096
  waitSeconds: "{seconds} 秒",
2097
+ toastKicker: "高峰价格",
2098
+ toastTitle: "当前 Token 价格为 2 倍",
2099
+ toastSoonTitle: "高峰价格将在 {wait} 后开始",
2100
+ toastMeta: "还剩 {wait} · {start}–{end} {zone}",
2101
+ toastSoonMeta: "将在 {wait} 后开始 · {start}–{end} {zone}",
2102
+ toastSchedule: "时间表",
2103
+ toastHide: "隐藏",
2104
+ toastDetails: "详情",
2105
+ toastVerified: "价格时段已核对 {date}",
2106
+ toastAria: "高峰价格提醒",
2059
2107
  };
2060
2108
 
2061
2109
  const inject = ["slots", "locale"];
@@ -2110,8 +2158,20 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2110
2158
  return `${formatClock(start)}–${formatClock(end)}`;
2111
2159
  }
2112
2160
 
2161
+ /** Beijing calendar date key; Chinese holiday ranges are defined in local dates. */
2162
+ function beijingDateKey(date) {
2163
+ const local = new Date(date.getTime() + 8 * 3600 * 1000);
2164
+ return `${local.getUTCFullYear()}-${pad(local.getUTCMonth() + 1)}-${pad(local.getUTCDate())}`;
2165
+ }
2166
+
2167
+ function isChineseHoliday(date) {
2168
+ const key = beijingDateKey(date);
2169
+ return HOLIDAY_RANGES.some(([from, to]) => key >= from && key <= to);
2170
+ }
2171
+
2113
2172
  /** Peak window containing `date`, or null. Weekdays are evaluated in UTC. */
2114
2173
  function peakWindowAt(date) {
2174
+ if (isChineseHoliday(date)) return null;
2115
2175
  const day = date.getUTCDay();
2116
2176
  if (day === 0 || day === 6) return null;
2117
2177
  const minutes = date.getUTCHours() * 60 + date.getUTCMinutes() + date.getUTCSeconds() / 60;
@@ -2133,9 +2193,9 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
2133
2193
  /** Next peak window start strictly after `date`. */
2134
2194
  function nextPeakWindow(date) {
2135
2195
  const probe = new Date(date.getTime());
2136
- for (let index = 0; index < 8; index += 1) {
2196
+ for (let index = 0; index < 20; index += 1) {
2137
2197
  const day = probe.getUTCDay();
2138
- if (day !== 0 && day !== 6) {
2198
+ if (day !== 0 && day !== 6 && !isChineseHoliday(probe)) {
2139
2199
  for (const window of WINDOWS_UTC) {
2140
2200
  const start = Date.UTC(probe.getUTCFullYear(), probe.getUTCMonth(), probe.getUTCDate(), window.from, 0, 0);
2141
2201
  if (start > date.getTime()) {
@@ -2416,6 +2476,37 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
2416
2476
  .dpg-btn[data-emphasis="true"]{background:var(--dpg-red);color:#2a0709}
2417
2477
  }
2418
2478
  @media (max-width:1100px){.dpg-badge-wait{display:none}}
2479
+ /* corner toast: a quiet, premium alert over the whole frame */
2480
+ .dpg-toast-wrap{position:fixed;right:22px;bottom:20px;z-index:1300;max-width:min(392px,calc(100vw - 24px));pointer-events:none}
2481
+ .dpg-toast{--dpg-toast-accent:var(--dsw-alias-state-error-primary,#e5484d);position:relative;display:flex;flex-direction:column;gap:9px;padding:13px 13px 11px;border-radius:17px;border:1px solid color-mix(in srgb,var(--dpg-toast-accent) 24%,transparent);background:linear-gradient(155deg,color-mix(in srgb,var(--dpg-toast-accent) 6%,var(--dsw-alias-bg-layer-1,#fff)) 0%,var(--dsw-alias-bg-layer-1,#fff) 62%);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 50%,transparent),0 22px 48px -28px color-mix(in srgb,var(--dpg-toast-accent) 78%,transparent);color:var(--dsw-alias-label-primary,#111);pointer-events:auto;animation:dpg-toast-in .32s cubic-bezier(.16,1,.3,1)}
2482
+ .dpg-toast[data-mode="soon"]{--dpg-toast-accent:var(--dsw-alias-state-warn-primary,#d97706)}
2483
+ .dpg-toast::before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:3px;border-radius:0 3px 3px 0;background:var(--dpg-toast-accent)}
2484
+ .dpg-toast-head{display:flex;align-items:flex-start;gap:10px;padding-left:5px}
2485
+ .dpg-toast-icon{flex:none;width:34px;height:34px;border-radius:11px;display:flex;align-items:center;justify-content:center;color:#fff;background:linear-gradient(150deg,var(--dpg-toast-accent),color-mix(in srgb,var(--dpg-toast-accent) 55%,#4a070c));box-shadow:0 7px 16px -9px color-mix(in srgb,var(--dpg-toast-accent) 95%,transparent),inset 0 1px 0 rgba(255,255,255,.3)}
2486
+ .dpg-toast[data-mode="soon"] .dpg-toast-icon{background:linear-gradient(150deg,var(--dpg-toast-accent),color-mix(in srgb,var(--dpg-toast-accent) 55%,#4a2c00));color:#fff}
2487
+ .dpg-toast-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1;padding-top:1px}
2488
+ .dpg-toast-kicker{display:flex;align-items:center;gap:6px;font-size:9.5px;font-weight:750;letter-spacing:.14em;text-transform:uppercase;color:color-mix(in srgb,var(--dpg-toast-accent) 82%,#111)}
2489
+ .dpg-toast-kicker .dpg-dot{width:5px;height:5px;box-shadow:0 0 0 3px color-mix(in srgb,var(--dpg-toast-accent) 13%,transparent)}
2490
+ .dpg-toast-chip{display:inline-flex;align-items:center;height:15px;padding:0 5px;border-radius:4px;border:1px solid color-mix(in srgb,var(--dpg-toast-accent) 40%,transparent);background:color-mix(in srgb,var(--dpg-toast-accent) 9%,transparent);font-size:9px;font-weight:800;letter-spacing:.03em}
2491
+ .dpg-toast-title{font-size:13.5px;line-height:18px;font-weight:660;letter-spacing:-.01em}
2492
+ .dpg-toast-meta{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#5b6472);font-variant-numeric:tabular-nums}
2493
+ .dpg-toast-close{flex:none;border:none;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);width:24px;height:24px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;cursor:pointer;margin:-2px -2px 0 0}
2494
+ .dpg-toast-close:hover{background:color-mix(in srgb,var(--dpg-toast-accent) 9%,transparent);color:var(--dpg-toast-accent)}
2495
+ .dpg-toast-close:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
2496
+ .dpg-toast-track{position:relative;height:3px;border-radius:2px;background:color-mix(in srgb,var(--dpg-toast-accent) 13%,transparent);overflow:hidden;margin:0 1px}
2497
+ .dpg-toast-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,color-mix(in srgb,var(--dpg-toast-accent) 62%,#fff) 0%,var(--dpg-toast-accent) 100%);transition:width .9s linear}
2498
+ .dpg-toast-actions{display:flex;align-items:center;gap:6px;padding-left:5px}
2499
+ .dpg-toast-btn{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:transparent;color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:10.5px;font-weight:650;line-height:14px;padding:5px 9px;border-radius:7px;cursor:pointer;transition:background .16s ease,border-color .16s ease}
2500
+ .dpg-toast-btn:hover{background:color-mix(in srgb,var(--dpg-toast-accent) 7%,transparent);border-color:color-mix(in srgb,var(--dpg-toast-accent) 34%,transparent)}
2501
+ .dpg-toast-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
2502
+ .dpg-toast-btn[data-emphasis="true"]{border-color:transparent;background:var(--dpg-toast-accent);color:#fff}
2503
+ .dpg-toast[data-mode="soon"] .dpg-toast-btn[data-emphasis="true"]{color:#2a1500}
2504
+ .dpg-toast-details{display:grid;grid-template-columns:1fr;gap:5px;padding:7px 9px 2px;border-top:1px solid color-mix(in srgb,var(--dpg-toast-accent) 14%,transparent);font-size:10.5px;line-height:15px;color:var(--dsw-alias-label-secondary,#5b6472)}
2505
+ .dpg-toast-details b{color:var(--dsw-alias-label-primary,#111);font-weight:650}
2506
+ .dpg-toast-details .dpg-toast-schedule{display:flex;flex-wrap:wrap;gap:5px;margin-top:2px}
2507
+ @keyframes dpg-toast-in{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}}
2508
+ @media (max-width:520px){.dpg-toast-wrap{right:10px;left:10px;bottom:calc(10px + env(safe-area-inset-bottom,0px));max-width:none}.dpg-toast{max-height:70vh;overflow:auto;padding:12px 12px 10px}}
2509
+ @media (prefers-reduced-motion:reduce){.dpg-toast{animation:none}.dpg-toast-fill{transition:none}}
2419
2510
  @media (max-width:900px){.dpg-badge{display:none}.dpg-mid{align-items:stretch;flex-direction:column;gap:8px}.dpg-count{min-width:0;flex-direction:row;align-items:baseline;gap:8px}.dpg-count-label{margin-top:0}.dpg-meter{padding-bottom:0}.dpg-meter-head{flex-wrap:wrap;row-gap:2px}.dpg-tail{align-items:stretch;flex-direction:column}.dpg-actions{display:flex}.dpg-actions .dpg-btn{flex:1;text-align:center}.dpg-seal{width:52px;height:52px}.dpg-ring{width:52px;height:52px}.dpg-title{font-size:14.5px;line-height:20px}}
2420
2511
  @media (max-width:520px){.dpg-card{position:fixed;left:10px;right:10px;bottom:calc(12px + env(safe-area-inset-bottom,0px));z-index:1200;max-height:78vh;overflow:auto;margin:0;padding:14px 15px 13px;gap:12px}.dpg-seal{display:none}.dpg-details{grid-template-columns:1fr}}
2421
2512
  @media (prefers-reduced-motion:reduce){.dpg-card{animation:none}.dpg-dot,.dpg-seal-pulse{animation:none}.dpg-fill,.dpg-ring-fill{transition:none}}
@@ -2592,6 +2683,121 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
2592
2683
  );
2593
2684
  }
2594
2685
 
2686
+ function PeakToast() {
2687
+ const state = usePeakState(store, t);
2688
+ const [dismissed, setDismissed] = useState(false);
2689
+ const [autoHidden, setAutoHidden] = useState(false);
2690
+ const [details, setDetails] = useState(false);
2691
+ const active = state.mode === "peak" || state.mode === "soon";
2692
+ const storageKey = state.mode === "peak" ? DISMISS_KEY : SOON_DISMISS_KEY;
2693
+ const marker = state.mode === "peak" ? state.windowEnd : state.windowStart;
2694
+ const readDismissed = () => {
2695
+ try {
2696
+ return Number(localStorage.getItem(storageKey) || "0") === Number(marker);
2697
+ } catch {
2698
+ return false;
2699
+ }
2700
+ };
2701
+ useEffect(() => {
2702
+ setDismissed(readDismissed());
2703
+ setAutoHidden(false);
2704
+ setDetails(false);
2705
+ if (!active) return undefined;
2706
+ const timer = setTimeout(() => setAutoHidden(true), 18000);
2707
+ return () => clearTimeout(timer);
2708
+ }, [state.mode, state.windowStart, state.windowEnd]);
2709
+ useEffect(() => {
2710
+ const onStorage = (event) => {
2711
+ if (event.key === storageKey) setDismissed(readDismissed());
2712
+ };
2713
+ const onKey = (event) => {
2714
+ if (event.key === "Escape" && active) setDismissed(true);
2715
+ };
2716
+ window.addEventListener("storage", onStorage);
2717
+ window.addEventListener("keydown", onKey);
2718
+ return () => {
2719
+ window.removeEventListener("storage", onStorage);
2720
+ window.removeEventListener("keydown", onKey);
2721
+ };
2722
+ }, [active, storageKey, state.mode, state.windowStart, state.windowEnd]);
2723
+ if (!active || dismissed || autoHidden) return null;
2724
+ const isSoon = state.mode === "soon";
2725
+ const close = () => {
2726
+ setDismissed(true);
2727
+ try {
2728
+ localStorage.setItem(storageKey, String(marker));
2729
+ } catch {
2730
+ /* private mode */
2731
+ }
2732
+ };
2733
+ const progress = Math.round((state.progress ?? 0) * 100);
2734
+ const meta = formatTemplate(isSoon ? t("toastSoonMeta") : t("toastMeta"), {
2735
+ wait: state.wait,
2736
+ start: state.startLabel,
2737
+ end: state.endLabel,
2738
+ zone: state.zone,
2739
+ });
2740
+ const title = isSoon ? formatTemplate(t("toastSoonTitle"), { wait: state.wait }) : t("toastTitle");
2741
+ return h(
2742
+ "div",
2743
+ { className: "dpg-toast-wrap" },
2744
+ h(
2745
+ "div",
2746
+ {
2747
+ className: "dpg-toast",
2748
+ "data-mode": isSoon ? "soon" : "peak",
2749
+ role: "status",
2750
+ "aria-live": "polite",
2751
+ "aria-atomic": "true",
2752
+ "aria-label": t("toastAria"),
2753
+ },
2754
+ h(
2755
+ "div",
2756
+ { className: "dpg-toast-head" },
2757
+ h("span", { className: "dpg-toast-icon", "aria-hidden": "true" }, h(IconBolt)),
2758
+ h(
2759
+ "div",
2760
+ { className: "dpg-toast-copy" },
2761
+ h(
2762
+ "div",
2763
+ { className: "dpg-toast-kicker" },
2764
+ h("span", { className: "dpg-dot", "aria-hidden": "true" }),
2765
+ isSoon ? t("soonKicker") : t("toastKicker"),
2766
+ h("span", { className: "dpg-toast-chip" }, t("multiplier")),
2767
+ ),
2768
+ h("div", { className: "dpg-toast-title" }, title),
2769
+ h("div", { className: "dpg-toast-meta" }, meta),
2770
+ ),
2771
+ h("button", { type: "button", className: "dpg-toast-close", onClick: close, "aria-label": t("toastHide"), title: t("toastHide") }, "×"),
2772
+ ),
2773
+ h(
2774
+ "div",
2775
+ { className: "dpg-toast-track", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progress, "aria-label": isSoon ? t("soonCountdownLabel") : t("countdownLabel") },
2776
+ h("span", { className: "dpg-toast-fill", style: { display: "block", width: `${progress}%` } }),
2777
+ ),
2778
+ details
2779
+ ? h(
2780
+ "div",
2781
+ { className: "dpg-toast-details" },
2782
+ h("span", null, isSoon ? t("soonReason") : t("reason")),
2783
+ h("span", { className: "dpg-toast-schedule" },
2784
+ h("b", null, t("scheduleChipLabel")),
2785
+ state.scheduleLocalParts.map((part) => h("span", { className: "dpg-chip", key: part }, part)),
2786
+ ),
2787
+ h("span", null, t("holidayNote")),
2788
+ h("span", null, formatTemplate(t("toastVerified"), { date: SCHEDULE_VERIFIED })),
2789
+ )
2790
+ : null,
2791
+ h(
2792
+ "div",
2793
+ { className: "dpg-toast-actions" },
2794
+ h("button", { type: "button", className: "dpg-toast-btn", onClick: () => setDetails((value) => !value), "aria-expanded": details ? "true" : "false" }, details ? t("hideDetails") : t("toastDetails")),
2795
+ h("button", { type: "button", className: "dpg-toast-btn", "data-emphasis": "true", onClick: close }, t("toastHide")),
2796
+ ),
2797
+ ),
2798
+ );
2799
+ }
2800
+
2595
2801
  ctx.effect(() => ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
2596
2802
  name: "conversation.composer.dock",
2597
2803
  id: "peak-guard",
@@ -2599,6 +2805,13 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
2599
2805
  locale: NS,
2600
2806
  }, PeakCard)), "peak-guard: composer dock card");
2601
2807
 
2808
+ ctx.effect(() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
2809
+ name: "shell.overlay",
2810
+ id: "peak-guard-toast",
2811
+ order: 70,
2812
+ locale: NS,
2813
+ }, PeakToast)), "peak-guard: corner toast");
2814
+
2602
2815
  ctx.effect(() => ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
2603
2816
  name: "conversation.session.header.utilities",
2604
2817
  id: "peak-guard",
@@ -2638,7 +2851,6 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
2638
2851
 
2639
2852
  exports.apply = apply;
2640
2853
  exports.inject = inject;
2641
-
2642
2854
  return module.exports;
2643
2855
  })());
2644
2856
 
@@ -3344,7 +3556,6 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3344
3556
 
3345
3557
  exports.apply = apply;
3346
3558
  exports.inject = inject;
3347
-
3348
3559
  return module.exports;
3349
3560
  })());
3350
3561
 
@@ -3421,8 +3632,17 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3421
3632
  groupSkillsDesc: "All file-based skills (without node_modules)",
3422
3633
  groupMemory: "Memory",
3423
3634
  groupMemoryDesc: "memory/ — session log, project dossiers, environment facts",
3424
- groupProjectsMemory: "Project memory",
3425
- groupProjectsMemoryDesc: "projects-memory/ — dossiers and journals",
3635
+ groupProjectsMemory: "Project memory (full backup)",
3636
+ groupProjectsMemoryDesc: "projects-memory/ — dossiers and journals. For a USB drive or GitHub use Settings → Project memory; this item is only part of a full profile backup.",
3637
+ modeTitle: "Transfer mode",
3638
+ modeFull: "Profile and environment",
3639
+ modeFullDesc: "Settings, plugins, skills and workspaces; project memory is separate",
3640
+ modeProject: "Projects and code",
3641
+ modeProjectDesc: "Source folders and project memory; settings stay untouched",
3642
+ projectExportTitle: "Save projects and code",
3643
+ projectExportSub: "Workspaces + source folders. Project memory is transferred separately in Project memory → USB.",
3644
+ projectExportButton: "Save to USB",
3645
+ projectModeHint: "Project memory dossiers live in Settings → Project memory → USB. This mode carries code and workspace folders.",
3426
3646
  groupConfig: "Global configs",
3427
3647
  groupConfigDesc: "AGENTS.md, dream-skin.json",
3428
3648
  groupSessions: "Session history",
@@ -3438,6 +3658,18 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3438
3658
  noteLabel: "Note",
3439
3659
  copiedPath: "Path copied",
3440
3660
  archivePathHint: "Use the path shown after export, or type the archive path from a USB drive / cloud folder.",
3661
+ usbTitle: "USB: projects and code",
3662
+ usbHint: "Save workspaces and source folders to a USB drive, then find the archive on the other machine.",
3663
+ usbPreset: "Select projects + code",
3664
+ usbPresetHint: "Workspaces, workspace folders and project memory; profile settings are left out.",
3665
+ usbPathLabel: "USB drive path",
3666
+ usbPathPlaceholder: "E:\\",
3667
+ usbScan: "Find archives",
3668
+ usbScanning: "Scanning…",
3669
+ usbNoArchives: "No dsh-profile-*.tar.gz archives found on this drive.",
3670
+ usbFound: "{count} archive(s) found",
3671
+ usbPick: "Check",
3672
+ usbArchiveMeta: "{size} · {date}",
3441
3673
  };
3442
3674
  const RU = {
3443
3675
  nav: "Перенос и резервные копии",
@@ -3498,8 +3730,17 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3498
3730
  groupSkillsDesc: "Все файловые навыки (без node_modules)",
3499
3731
  groupMemory: "Память",
3500
3732
  groupMemoryDesc: "memory/ — журнал сессий, досье проектов, факты окружения",
3501
- groupProjectsMemory: "Память проектов",
3502
- groupProjectsMemoryDesc: "projects-memory/ — досье и журналы",
3733
+ groupProjectsMemory: "Память проектов (полный бэкап)",
3734
+ groupProjectsMemoryDesc: "projects-memory/ — досье и журналы. Для флешки и GitHub используйте «Память проектов»; этот пункт нужен только для полного бэкапа профиля.",
3735
+ modeTitle: "Режим переноса",
3736
+ modeFull: "Профиль и окружение",
3737
+ modeFullDesc: "Настройки, плагины, навыки и рабочие области; память проектов — отдельно",
3738
+ modeProject: "Проекты и код",
3739
+ modeProjectDesc: "Папки с исходниками и память проектов; настройки не трогаем",
3740
+ projectExportTitle: "Сохранить проекты и код",
3741
+ projectExportSub: "Рабочие области + папки с исходниками. Память проектов переносится отдельно, в «Память проектов → Флешка».",
3742
+ projectExportButton: "Сохранить на флешку",
3743
+ projectModeHint: "Досье памяти проектов — в «Память проектов → Флешка». Здесь — код и рабочие папки.",
3503
3744
  groupConfig: "Глобальные конфиги",
3504
3745
  groupConfigDesc: "AGENTS.md, dream-skin.json",
3505
3746
  groupSessions: "История сессий",
@@ -3515,6 +3756,18 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3515
3756
  noteLabel: "Примечание",
3516
3757
  copiedPath: "Путь скопирован",
3517
3758
  archivePathHint: "Используйте путь из результата экспорта или впишите путь к архиву с флешки / из облака.",
3759
+ usbTitle: "Флешка: проекты и код",
3760
+ usbHint: "Сохраните рабочие области и папки с исходниками на флешку, а на другой машине найдите архив и восстановите.",
3761
+ usbPreset: "Выбрать «Проекты и код»",
3762
+ usbPresetHint: "Рабочие области, папки с кодом и память проектов; настройки профиля не затрагиваются.",
3763
+ usbPathLabel: "Путь к флешке",
3764
+ usbPathPlaceholder: "E:\\",
3765
+ usbScan: "Найти архивы",
3766
+ usbScanning: "Сканирую…",
3767
+ usbNoArchives: "На флешке не найдено архивов dsh-profile-*.tar.gz.",
3768
+ usbFound: "Найдено архивов: {count}",
3769
+ usbPick: "Проверить",
3770
+ usbArchiveMeta: "{size} · {date}",
3518
3771
  };
3519
3772
  const ZH = {
3520
3773
  nav: "迁移与备份",
@@ -3575,8 +3828,17 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3575
3828
  groupSkillsDesc: "全部文件型技能(不含 node_modules)",
3576
3829
  groupMemory: "记忆",
3577
3830
  groupMemoryDesc: "memory/ — 会话日志、项目档案、环境事实",
3578
- groupProjectsMemory: "项目记忆",
3579
- groupProjectsMemoryDesc: "projects-memory/ — 档案与日志",
3831
+ groupProjectsMemory: "项目记忆(完整备份)",
3832
+ groupProjectsMemoryDesc: "projects-memory/ — 档案与日志。U 盘或 GitHub 请使用「项目记忆」;此项仅用于完整配置备份。",
3833
+ modeTitle: "传输模式",
3834
+ modeFull: "配置与环境",
3835
+ modeFullDesc: "设置、插件、技能和工作区;项目记忆单独传输",
3836
+ modeProject: "项目与代码",
3837
+ modeProjectDesc: "源码文件夹和项目记忆;不修改设置",
3838
+ projectExportTitle: "保存项目与代码",
3839
+ projectExportSub: "工作区 + 源码文件夹。项目记忆在「项目记忆 → U 盘」中单独传输。",
3840
+ projectExportButton: "保存到 U 盘",
3841
+ projectModeHint: "项目记忆档案在「项目记忆 → U 盘」。此处传输代码和工作区文件夹。",
3580
3842
  groupConfig: "全局配置",
3581
3843
  groupConfigDesc: "AGENTS.md、dream-skin.json",
3582
3844
  groupSessions: "会话历史",
@@ -3592,6 +3854,18 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3592
3854
  noteLabel: "说明",
3593
3855
  copiedPath: "路径已复制",
3594
3856
  archivePathHint: "使用导出结果中的路径,或输入 U 盘 / 云盘中的归档路径。",
3857
+ usbTitle: "U 盘:项目与代码",
3858
+ usbHint: "将工作区和源码文件夹保存到 U 盘,在另一台机器上找到归档并恢复。",
3859
+ usbPreset: "选择「项目 + 代码」",
3860
+ usbPresetHint: "工作区、工作区文件夹和项目记忆;不包含配置设置。",
3861
+ usbPathLabel: "U 盘路径",
3862
+ usbPathPlaceholder: "E:\\",
3863
+ usbScan: "查找归档",
3864
+ usbScanning: "扫描中…",
3865
+ usbNoArchives: "U 盘上没有 dsh-profile-*.tar.gz 归档。",
3866
+ usbFound: "找到 {count} 个归档",
3867
+ usbPick: "检查",
3868
+ usbArchiveMeta: "{size} · {date}",
3595
3869
  };
3596
3870
 
3597
3871
  const inject = ["slots", "locale", "connection"];
@@ -3658,9 +3932,34 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3658
3932
  .dpt-step-num{font-variant-numeric:tabular-nums;font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--dsw-alias-brand-primary,#2f7df6)}
3659
3933
  .dpt-step-label{font-size:12px;font-weight:600;letter-spacing:.04em;color:var(--dsw-alias-label-secondary,#5b6472);text-transform:uppercase}
3660
3934
  .dpt-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))}
3661
- .dpt-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
3935
+ .dpt-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
3662
3936
  .dpt-card-title{font-size:15px;line-height:20px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
3663
3937
  .dpt-card-sub{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472);margin-top:3px;max-width:62ch}
3938
+ .dpt-modes{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:2px 0 4px}
3939
+ .dpt-mode{display:flex;align-items:flex-start;gap:10px;text-align:left;padding:11px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#111);cursor:pointer;font:inherit;transition:border-color .16s ease,background .16s ease,box-shadow .16s ease}
3940
+ .dpt-mode:hover{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 35%,transparent)}
3941
+ .dpt-mode:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:2px}
3942
+ .dpt-mode[data-active="true"]{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 48%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 6%,var(--dsw-alias-bg-base,#fff));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 14%,transparent)}
3943
+ .dpt-mode-icon{flex:none;width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 6%,transparent);color:var(--dsw-alias-label-secondary,#5b6472)}
3944
+ .dpt-mode[data-active="true"] .dpt-mode-icon{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 13%,transparent);color:var(--dsw-alias-brand-primary,#2f7df6)}
3945
+ .dpt-mode-copy{display:flex;flex-direction:column;gap:2px;min-width:0}
3946
+ .dpt-mode-title{font-size:12.5px;line-height:17px;font-weight:650}
3947
+ .dpt-mode-desc{font-size:10.5px;line-height:14px;color:var(--dsw-alias-label-secondary,#6b7280)}
3948
+ @media (max-width:640px){.dpt-modes{grid-template-columns:1fr}}
3949
+ .dpt-kv{flex-wrap:wrap;min-width:0}
3950
+ .dpt-kv span{min-width:0}
3951
+ .dpt-mono{overflow-wrap:anywhere}
3952
+ .dpt-row-inline{min-width:0}
3953
+ @media (max-width:560px){
3954
+ .dpt-kv{flex-direction:column;align-items:flex-start;gap:3px}
3955
+ .dpt-kv span:last-child{text-align:left}
3956
+ .dpt-row-inline{width:100%;justify-content:flex-start}
3957
+ .dpt-card{padding:14px 13px 13px}
3958
+ .dpt-input,.dpt-btn{max-width:100%}
3959
+ .dpt-row{flex-wrap:wrap}
3960
+ .dpt-row-text{flex:1 1 calc(100% - 27px);min-width:0}
3961
+ .dpt-row-size{width:100%;padding-left:27px;text-align:left;white-space:normal}
3962
+ }
3664
3963
  .dpt-chip{flex:none;font-size:11px;font-weight:600;letter-spacing:.04em;padding:3px 8px;border-radius:999px;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}
3665
3964
  .dpt-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
3666
3965
  .dpt-row{display:flex;align-items:flex-start;gap:12px;padding:10px 2px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07));cursor:pointer}
@@ -3697,6 +3996,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3697
3996
  .dpt-kv{display:flex;justify-content:space-between;gap:16px;font-size:12px;line-height:18px}
3698
3997
  .dpt-kv span:first-child{color:var(--dsw-alias-label-secondary,#6b7280)}
3699
3998
  .dpt-kv span:last-child{color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums;text-align:right}
3999
+ .dpt-row-inline{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
3700
4000
  .dpt-divider{height:1px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.08));margin:2px 0}
3701
4001
  .dpt-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
3702
4002
  @media (max-width:560px){.dpt-grid2{grid-template-columns:1fr}.dpt-steps{flex-direction:column}.dpt-step + .dpt-step{border-left:none;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));padding-left:0}}
@@ -3769,6 +4069,10 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3769
4069
  const [confirmed, setConfirmed] = useState(false);
3770
4070
  const [imported, setImported] = useState(null);
3771
4071
  const [copied, setCopied] = useState(false);
4072
+ const [usbPath, setUsbPath] = useState("");
4073
+ const [usbArchives, setUsbArchives] = useState([]);
4074
+ const [usbBusy, setUsbBusy] = useState(false);
4075
+ const [mode, setMode] = useState("full");
3772
4076
 
3773
4077
  useEffect(() => {
3774
4078
  let alive = true;
@@ -3782,7 +4086,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3782
4086
  }
3783
4087
  const value = result.value;
3784
4088
  setOptions(value);
3785
- setSelected({ ...value.selection });
4089
+ setSelected({ ...value.selection, projectsMemory: false });
3786
4090
  setDestination(value.destination ?? "");
3787
4091
  setPhase("ready");
3788
4092
  })
@@ -3803,7 +4107,42 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3803
4107
  .filter((row) => row !== undefined);
3804
4108
  }, [options]);
3805
4109
 
3806
- const selectedCount = Object.values(selected).filter(Boolean).length;
4110
+ const visibleGroups = useMemo(
4111
+ () =>
4112
+ groups.filter(
4113
+ (group) =>
4114
+ group.id !== "projectsMemory" &&
4115
+ (mode === "full" || group.id === "workspaces" || group.id === "workspaceFolders"),
4116
+ ),
4117
+ [groups, mode],
4118
+ );
4119
+
4120
+ const selectedCount = visibleGroups.filter((group) => selected[group.id] === true).length;
4121
+
4122
+ function switchMode(next) {
4123
+ setMode(next);
4124
+ if (next === "project") {
4125
+ setSelected({
4126
+ profile: false,
4127
+ localPlugins: false,
4128
+ workspaces: true,
4129
+ workspaceFolders: true,
4130
+ skills: false,
4131
+ memory: false,
4132
+ projectsMemory: false,
4133
+ config: false,
4134
+ sessions: false,
4135
+ attachments: false,
4136
+ credentials: false,
4137
+ });
4138
+ } else {
4139
+ setSelected({ ...(options?.selection ?? {}), projectsMemory: false });
4140
+ }
4141
+ setPreview(null);
4142
+ setImported(null);
4143
+ setMessage(null);
4144
+ if (next === "project" && usbPath.trim().length > 0) setDestination(usbPath.trim());
4145
+ }
3807
4146
 
3808
4147
  function toggle(id, value) {
3809
4148
  setSelected((current) => ({ ...current, [id]: value }));
@@ -3833,18 +4172,20 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3833
4172
  }
3834
4173
  }
3835
4174
 
3836
- async function doInspect() {
4175
+ async function doInspect(targetPath) {
3837
4176
  if (busy !== null) return;
3838
- if (archivePath.trim().length === 0) {
4177
+ const requested = typeof targetPath === "string" ? targetPath.trim() : archivePath.trim();
4178
+ if (requested.length === 0) {
3839
4179
  setMessage({ tone: "error", text: t("missingArchive") });
3840
4180
  return;
3841
4181
  }
4182
+ setArchivePath(requested);
3842
4183
  setBusy("inspect");
3843
4184
  setMessage(null);
3844
4185
  setPreview(null);
3845
4186
  setImported(null);
3846
4187
  try {
3847
- const result = await call("inspect", { archivePath: archivePath.trim() });
4188
+ const result = await call("inspect", { archivePath: requested });
3848
4189
  if (!result?.ok) {
3849
4190
  setMessage({ tone: "error", text: `${t("errorPrefix")} ${result?.error?.message ?? "unknown"}` });
3850
4191
  return;
@@ -3857,6 +4198,34 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3857
4198
  }
3858
4199
  }
3859
4200
 
4201
+ async function findArchives() {
4202
+ if (busy !== null || usbBusy) return;
4203
+ if (usbPath.trim().length === 0) {
4204
+ setMessage({ tone: "error", text: t("usbPathPlaceholder") });
4205
+ return;
4206
+ }
4207
+ setUsbBusy(true);
4208
+ setMessage(null);
4209
+ try {
4210
+ const result = await call("find-archives", { directory: usbPath.trim() });
4211
+ if (!result?.ok) {
4212
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${result?.error?.message ?? "unknown"}` });
4213
+ setUsbArchives([]);
4214
+ return;
4215
+ }
4216
+ const archives = Array.isArray(result.value?.archives) ? result.value.archives : [];
4217
+ setUsbArchives(archives);
4218
+ setMessage({
4219
+ tone: archives.length > 0 ? "ok" : "error",
4220
+ text: archives.length > 0 ? t("usbFound").replace("{count}", String(archives.length)) : t("usbNoArchives"),
4221
+ });
4222
+ } catch (error) {
4223
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${String(error?.message ?? error)}` });
4224
+ } finally {
4225
+ setUsbBusy(false);
4226
+ }
4227
+ }
4228
+
3860
4229
  async function doImport() {
3861
4230
  if (busy !== null) return;
3862
4231
  if (!confirmed) {
@@ -3913,7 +4282,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3913
4282
  const stepOneGroups = h(
3914
4283
  "div",
3915
4284
  { className: "dpt-list" },
3916
- groups.map((group) => h(GroupRow, {
4285
+ visibleGroups.map((group) => h(GroupRow, {
3917
4286
  key: group.id,
3918
4287
  group,
3919
4288
  checked: selected[group.id] === true,
@@ -3942,12 +4311,48 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3942
4311
  "div",
3943
4312
  { className: "dpt-card-head" },
3944
4313
  h("div", null,
3945
- h("div", { className: "dpt-card-title", id: "dpt-export-title" }, t("exportTitle")),
3946
- h("div", { className: "dpt-card-sub" }, t("exportSubtitle")),
4314
+ h("div", { className: "dpt-card-title", id: "dpt-export-title" }, mode === "project" ? t("projectExportTitle") : t("exportTitle")),
4315
+ h("div", { className: "dpt-card-sub" }, mode === "project" ? t("projectExportSub") : t("exportSubtitle")),
3947
4316
  ),
3948
- h("span", { className: "dpt-chip" }, phase === "loading" ? "…" : `${selectedCount} / ${groups.length}`),
4317
+ h("span", { className: "dpt-chip" }, phase === "loading" ? "…" : `${selectedCount} / ${visibleGroups.length}`),
3949
4318
  ),
3950
4319
  phase === "loading" && h("div", { className: "dpt-hint" }, t("optionsLoading")),
4320
+ phase !== "loading"
4321
+ ? h(
4322
+ "div",
4323
+ { className: "dpt-modes", role: "tablist", "aria-label": t("modeTitle") },
4324
+ [
4325
+ ["full", "modeFull", "modeFullDesc"],
4326
+ ["project", "modeProject", "modeProjectDesc"],
4327
+ ].map(([id, titleKey, descKey]) =>
4328
+ h(
4329
+ "button",
4330
+ {
4331
+ key: id,
4332
+ type: "button",
4333
+ role: "tab",
4334
+ className: "dpt-mode",
4335
+ "data-active": mode === id ? "true" : "false",
4336
+ "aria-selected": mode === id ? "true" : "false",
4337
+ onClick: () => switchMode(id),
4338
+ },
4339
+ h(
4340
+ "span",
4341
+ { className: "dpt-mode-icon", "aria-hidden": "true" },
4342
+ id === "full"
4343
+ ? h("svg", { viewBox: "0 0 20 20", width: 17, height: 17, fill: "none" }, h("path", { d: "M3.5 6.5h13v8h-13zM3.5 6.5 5 3.5h10l1.5 3M6.5 14v2h7v-2", stroke: "currentColor", strokeWidth: 1.4, strokeLinejoin: "round" }))
4344
+ : h("svg", { viewBox: "0 0 20 20", width: 17, height: 17, fill: "none" }, h("path", { d: "M3.5 5.5h4l1.5 2h7.5v7.5h-13zM3.5 5.5V4h5l1.5 1.5", stroke: "currentColor", strokeWidth: 1.4, strokeLinejoin: "round" })),
4345
+ ),
4346
+ h(
4347
+ "span",
4348
+ { className: "dpt-mode-copy" },
4349
+ h("span", { className: "dpt-mode-title" }, t(titleKey)),
4350
+ h("span", { className: "dpt-mode-desc" }, t(descKey)),
4351
+ ),
4352
+ ),
4353
+ ),
4354
+ )
4355
+ : null,
3951
4356
  phase !== "loading" && stepOneGroups,
3952
4357
  h(
3953
4358
  "div",
@@ -3970,7 +4375,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3970
4375
  "button",
3971
4376
  { type: "button", className: "dpt-btn", "data-tone": "primary", disabled: busy !== null || phase !== "ready", onClick: doExport },
3972
4377
  busy === "export" ? h(Spinner) : null,
3973
- busy === "export" ? t("exporting") : t("exportButton"),
4378
+ busy === "export" ? t("exporting") : mode === "project" ? t("projectExportButton") : t("exportButton"),
3974
4379
  ),
3975
4380
  exported !== null
3976
4381
  ? h(
@@ -3995,6 +4400,72 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
3995
4400
  : null,
3996
4401
  ),
3997
4402
 
4403
+ /* ---------------- USB / projects and code ---------------- */
4404
+ mode === "project"
4405
+ ? h(
4406
+ "section",
4407
+ { className: "dpt-card", "aria-labelledby": "dpt-usb-title" },
4408
+ h(
4409
+ "div",
4410
+ { className: "dpt-card-head" },
4411
+ h(
4412
+ "div",
4413
+ null,
4414
+ h("div", { className: "dpt-card-title", id: "dpt-usb-title" }, t("usbTitle")),
4415
+ h("div", { className: "dpt-card-sub" }, t("usbHint")),
4416
+ ),
4417
+ h("span", { className: "dpt-chip" }, "USB"),
4418
+ ),
4419
+ h(
4420
+ "div",
4421
+ { className: "dpt-field" },
4422
+ h("label", { className: "dpt-label", htmlFor: "dpt-usb-path" }, t("usbPathLabel")),
4423
+ h("input", {
4424
+ id: "dpt-usb-path",
4425
+ className: "dpt-input dpt-input--path",
4426
+ type: "text",
4427
+ value: usbPath,
4428
+ placeholder: t("usbPathPlaceholder"),
4429
+ spellCheck: false,
4430
+ onChange: (event) => setUsbPath(event.target.value),
4431
+ }),
4432
+ ),
4433
+ h(
4434
+ "div",
4435
+ { className: "dpt-actions" },
4436
+ h(
4437
+ "button",
4438
+ { type: "button", className: "dpt-btn", disabled: busy !== null || usbBusy, onClick: findArchives },
4439
+ usbBusy ? h(Spinner) : null,
4440
+ usbBusy ? t("usbScanning") : t("usbScan"),
4441
+ ),
4442
+ ),
4443
+ usbArchives.length > 0
4444
+ ? h(
4445
+ "div",
4446
+ { className: "dpt-preview" },
4447
+ usbArchives.map((archive) =>
4448
+ h(
4449
+ "div",
4450
+ { className: "dpt-kv", key: archive.path },
4451
+ h("span", { className: "dpt-mono" }, archive.name),
4452
+ h(
4453
+ "span",
4454
+ { className: "dpt-row-inline" },
4455
+ h("span", null, t("usbArchiveMeta").replace("{size}", archive.human ?? "").replace("{date}", new Date(archive.mtimeMs).toLocaleDateString())),
4456
+ h(
4457
+ "button",
4458
+ { type: "button", className: "dpt-btn", disabled: busy !== null, onClick: () => void doInspect(archive.path) },
4459
+ t("usbPick"),
4460
+ ),
4461
+ ),
4462
+ ),
4463
+ ),
4464
+ )
4465
+ : null,
4466
+ )
4467
+ : null,
4468
+
3998
4469
  /* ---------------- import ---------------- */
3999
4470
  h(
4000
4471
  "section",
@@ -4143,11 +4614,2629 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
4143
4614
 
4144
4615
  exports.apply = apply;
4145
4616
  exports.inject = inject;
4146
-
4147
4617
  return module.exports;
4148
4618
  })());
4149
4619
 
4150
- const __partNames = ["dsh-gkb-ru-ui","dsh-token-lab","dsh-peak-guard","dsh-session-move","dsh-profile-transfer"];
4620
+ /* ---- dsh-memory-bridge ---- */
4621
+ __parts.push((function () {
4622
+ var module = { exports: {} };
4623
+ var exports = module.exports;
4624
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4625
+ const React = require("react");
4626
+ const h = React.createElement;
4627
+ const useEffect = React.useEffect;
4628
+ const useRef = React.useRef;
4629
+ const useState = React.useState;
4630
+
4631
+ const NS = "memory-bridge";
4632
+ const RPC_NS = "dsh-memory-bridge";
4633
+ const ALLOWED_INTERVALS = [60, 300, 600, 1800];
4634
+ const REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u;
4635
+
4636
+ const EN = {
4637
+ nav: "Project memory",
4638
+ title: "Project memory",
4639
+ subtitle: "Project-memory dossiers are synchronised through your own private GitHub repository. No commands are needed — every action is a button on this page.",
4640
+ langProbe: "en",
4641
+ loading: "Reading sync state…",
4642
+ loadError: "Could not read the sync state",
4643
+ retry: "Retry",
4644
+ errorPrefix: "Could not complete:",
4645
+ syncErrorLabel: "Sync error",
4646
+ never: "not yet",
4647
+ secRepo: "Repository",
4648
+ secSync: "Synchronisation",
4649
+ secHow: "How it works",
4650
+ secSecurity: "Security",
4651
+ secConflicts: "Conflicts",
4652
+ secProjects: "Projects",
4653
+ ghMissingTitle: "GitHub CLI is required",
4654
+ ghMissingText: "The plugin works through GitHub CLI. Install it on this machine and sign in once.",
4655
+ ghInstallStep: "Install GitHub CLI",
4656
+ ghInstallHint: "Download and install GitHub CLI (gh) from cli.github.com.",
4657
+ ghLoginStep: "Run in a terminal",
4658
+ ghLoginHint: "Sign-in is needed once on each machine.",
4659
+ checkAgain: "Check again",
4660
+ checking: "Checking…",
4661
+ ghLoginTitle: "Sign in to GitHub",
4662
+ ghLoginText: "GitHub CLI is installed, but you are not signed in yet.",
4663
+ repoSetupTitle: "Connect a repository",
4664
+ repoSetupText: "Dossiers are synchronised through your own private GitHub repository.",
4665
+ ghLoggedIn: "GitHub: signed in as",
4666
+ accountHint: "The repository will be created under your account — your dossiers will not go to someone else's repository.",
4667
+ repoLabel: "Repository",
4668
+ repoPlaceholder: "owner/name",
4669
+ createRepo: "Create private repository",
4670
+ creatingRepo: "Creating…",
4671
+ connectExisting: "Connect an existing private one",
4672
+ connectingRepo: "Connecting…",
4673
+ repoFieldHint: "owner/name, private only; the plugin checks visibility.",
4674
+ repoInvalid: "Enter the repository as owner/name.",
4675
+ privateOnlyNote: "The plugin refuses to work with a public repository.",
4676
+ configureDone: "Repository connected",
4677
+ createDone: "Private repository created and connected",
4678
+ gitMissing: "Git was not found on this machine. Synchronisation cannot push changes without it.",
4679
+ ghNotAuthorizedWarn: "GitHub CLI is not authorised — synchronisation cannot push changes.",
4680
+ stateSynced: "Everything is synchronised",
4681
+ stateLocalChanges: "There are local changes",
4682
+ conflictOne: "conflict",
4683
+ conflictFew: "conflicts",
4684
+ conflictMany: "conflicts",
4685
+ visPrivate: "Private",
4686
+ visPublic: "PUBLIC — disable it!",
4687
+ visUnknown: "Visibility not confirmed",
4688
+ publicRepoWarn: "This repository is public. Disable synchronisation: project-memory dossiers must not be visible to outsiders.",
4689
+ lastSync: "Last synchronisation",
4690
+ branch: "Branch",
4691
+ pendingLocal: "Local changes",
4692
+ pendingUnpushed: "Unpushed changes",
4693
+ enabledBadge: "Enabled",
4694
+ disabledBadge: "Disabled",
4695
+ syncNow: "Synchronise now",
4696
+ syncing: "Synchronising…",
4697
+ enable: "Enable",
4698
+ disable: "Disable",
4699
+ enabledDone: "Automatic sync enabled",
4700
+ disabledDone: "Automatic sync disabled",
4701
+ openRepo: "Open repository",
4702
+ howButton: "How it works",
4703
+ howShow: "Show",
4704
+ howHide: "Collapse",
4705
+ how1Title: "Private repository",
4706
+ how1Text: "One private repository on your GitHub account keeps a shared copy of the project-memory dossiers.",
4707
+ how2Title: "Three-way merge",
4708
+ how2Text: "On every sync a machine compares “mine / previous common / from GitHub” and pushes the result back.",
4709
+ how3Title: "Nothing is overwritten silently",
4710
+ how3Text: "Additions from different workstations are merged. If the same line changed differently, a conflict appears and you choose the variant.",
4711
+ sec1Title: "Private repository only",
4712
+ sec1Text: "The plugin checks visibility and refuses to work with a public repository.",
4713
+ sec2Title: "Secrets and PII are stripped",
4714
+ sec2Text: "Sensitive values are removed and home paths are anonymised.",
4715
+ sec3Title: "Conflicts never overwrite the file",
4716
+ sec3Text: "Until you pick a variant, the local file stays untouched.",
4717
+ sec4Title: "Backup and Git history",
4718
+ sec4Text: "Every change keeps a backup and is visible in the Git history.",
4719
+ optionsShow: "Options",
4720
+ optionsHide: "Hide options",
4721
+ optionsTitle: "Sync options",
4722
+ autoSync: "Automatic sync",
4723
+ autoSyncHint: "Synchronise on dossier changes and by the timer.",
4724
+ intervalLabel: "Interval",
4725
+ secondsShort: "s",
4726
+ excludeLabel: "Excluded projects",
4727
+ excludePlaceholder: "slug-one, slug-two",
4728
+ excludeHint: "Project slugs separated by commas. Their dossiers are not synchronised.",
4729
+ saveOptions: "Save",
4730
+ saving: "Saving…",
4731
+ optionsSaved: "Options saved",
4732
+ syncDone: "Synchronisation finished",
4733
+ summaryProjects: "Projects",
4734
+ summaryFiles: "Files",
4735
+ summaryConflicts: "Conflicts",
4736
+ summaryPushed: "Changes pushed to GitHub",
4737
+ summaryLocalOnly: "Saved locally, push is pending",
4738
+ conflictsTitle: "What to choose?",
4739
+ conflictsSubtitle: "The same record changed here and on GitHub. Choose what to keep.",
4740
+ conflictMine: "Mine",
4741
+ conflictRemote: "From GitHub",
4742
+ conflictKeepMine: "Keep mine",
4743
+ conflictTakeRemote: "Take from GitHub",
4744
+ conflictMerge: "Merge",
4745
+ hintKeepMine: "“Keep mine” — sends your version to GitHub",
4746
+ hintTakeRemote: "“Take from GitHub” — replaces this file with the repository version",
4747
+ hintMerge: "“Merge” — keeps both variants",
4748
+ conflictWorking: "Applying…",
4749
+ conflictDone: "Conflict resolved",
4750
+ conflictFound: "Found",
4751
+ previewEmpty: "empty",
4752
+ projectsEmpty: "Open a project folder in DSH — its dossier will appear here automatically.",
4753
+ statusSynced: "Synchronised",
4754
+ statusLocalChanges: "Local changes",
4755
+ statusConflict: "Conflict",
4756
+ statusNotOpened: "Not opened locally",
4757
+ historyOpen: "History",
4758
+ historyTitle: "Version history",
4759
+ historySubtitle: "Commits that changed this project dossier. Restore creates a new commit, so nothing is lost.",
4760
+ historyEmpty: "No versions yet — the first sync will create them.",
4761
+ historyLoading: "Loading history…",
4762
+ historyClose: "Close",
4763
+ historyRestore: "Restore",
4764
+ historyRestoreHint: "Restores the dossier files that existed in this commit; newer files stay in place. Current files are backed up first.",
4765
+ historyConfirm: "Restore the project dossier to this version? Current files will be backed up and a new commit will be created.",
4766
+ historyDone: "Version restored",
4767
+ historyAuthor: "by",
4768
+ diffLegend: "Changed lines are highlighted: pink — only in mine, green — only on GitHub",
4769
+ portableTitle: "USB transfer",
4770
+ portableHeading: "Take project memory with you",
4771
+ portableSubtitle: "Save dossiers to a USB drive and restore them on another machine — no GitHub, no commands.",
4772
+ portablePlaceholder: "E:\\ (USB drive or folder)",
4773
+ portableScan: "Scan",
4774
+ portableExport: "Save to USB",
4775
+ portableImport: "Import",
4776
+ portableHint: "The drive contains redacted Markdown dossiers without secrets or AUTO-SCAN blocks. For sensitive data, enable BitLocker on the drive.",
4777
+ portableEmpty: "No dsh-memory-portable folder found on this drive yet.",
4778
+ portableFound: "{count} projects on the drive",
4779
+ portableConfirm: "Import projects from the USB drive? Current dossiers will be backed up first.",
4780
+ portableImported: "{count} projects imported",
4781
+ portableExported: "{count} projects saved to the drive",
4782
+ portableScanFirst: "Scan the USB drive first.",
4783
+ portableNoPath: "Enter the USB drive path.",
4784
+ portableNew: "new",
4785
+ portableUpdate: "update",
4786
+ portableSame: "up to date",
4787
+ portableImportedState: "imported",
4788
+ portableImportedNew: "restored",
4789
+ portableSkipped: "skipped",
4790
+ portableOnlyMemory: "Memory dossiers only",
4791
+ portableCross: "Source code and workspace folders travel separately: Backup & transfer → Projects and code.",
4792
+ portableStepSave: "Save",
4793
+ portableStepScan: "Scan",
4794
+ portableStepImport: "Restore",
4795
+ };
4796
+ const RU = {
4797
+ nav: "Память проектов",
4798
+ title: "Память проектов",
4799
+ subtitle: "Досье памяти проектов синхронизируются через ваш собственный приватный GitHub-репозиторий. Команды не нужны — всё делается кнопками на этой странице.",
4800
+ langProbe: "ru",
4801
+ loading: "Читаю состояние синхронизации…",
4802
+ loadError: "Не удалось получить состояние синхронизации",
4803
+ retry: "Повторить",
4804
+ errorPrefix: "Не удалось выполнить:",
4805
+ syncErrorLabel: "Ошибка синхронизации",
4806
+ never: "ещё не выполнялась",
4807
+ secRepo: "Репозиторий",
4808
+ secSync: "Синхронизация",
4809
+ secHow: "Как это работает",
4810
+ secSecurity: "Безопасность",
4811
+ secConflicts: "Конфликты",
4812
+ secProjects: "Проекты",
4813
+ ghMissingTitle: "Нужен GitHub CLI",
4814
+ ghMissingText: "Плагин работает через GitHub CLI. Установите его на этой машине и один раз войдите в аккаунт.",
4815
+ ghInstallStep: "Установите GitHub CLI",
4816
+ ghInstallHint: "Скачайте и установите GitHub CLI (gh) с cli.github.com.",
4817
+ ghLoginStep: "Выполните в терминале",
4818
+ ghLoginHint: "Вход нужен один раз на каждой машине.",
4819
+ checkAgain: "Проверить снова",
4820
+ checking: "Проверяю…",
4821
+ ghLoginTitle: "Войдите в GitHub",
4822
+ ghLoginText: "GitHub CLI установлен, но вход ещё не выполнен.",
4823
+ repoSetupTitle: "Подключение репозитория",
4824
+ repoSetupText: "Досье будут синхронизироваться через ваш собственный приватный репозиторий.",
4825
+ ghLoggedIn: "GitHub: вы вошли как",
4826
+ accountHint: "Репозиторий будет создан под вашим аккаунтом — ваши досье не попадут в чужие репозитории.",
4827
+ repoLabel: "Репозиторий",
4828
+ repoPlaceholder: "owner/name",
4829
+ createRepo: "Создать приватный репозиторий",
4830
+ creatingRepo: "Создаю…",
4831
+ connectExisting: "Подключить существующий приватный",
4832
+ connectingRepo: "Подключаю…",
4833
+ repoFieldHint: "owner/name, только private; плагин проверит видимость.",
4834
+ repoInvalid: "Укажите репозиторий в формате owner/name.",
4835
+ privateOnlyNote: "Плагин отказывается работать с публичным репозиторием.",
4836
+ configureDone: "Репозиторий подключён",
4837
+ createDone: "Приватный репозиторий создан и подключён",
4838
+ gitMissing: "Git не найден на этой машине. Без него синхронизация не сможет отправлять изменения.",
4839
+ ghNotAuthorizedWarn: "GitHub CLI не авторизован — синхронизация не сможет отправлять изменения.",
4840
+ stateSynced: "Всё синхронизировано",
4841
+ stateLocalChanges: "Есть локальные изменения",
4842
+ conflictOne: "конфликт",
4843
+ conflictFew: "конфликта",
4844
+ conflictMany: "конфликтов",
4845
+ visPrivate: "Приватный",
4846
+ visPublic: "ПУБЛИЧНЫЙ — отключите!",
4847
+ visUnknown: "Видимость не подтверждена",
4848
+ publicRepoWarn: "Репозиторий публичный. Отключите синхронизацию: досье памяти не должны быть видны посторонним.",
4849
+ lastSync: "Последняя синхронизация",
4850
+ branch: "Ветка",
4851
+ pendingLocal: "Локальных правок",
4852
+ pendingUnpushed: "Есть неотправленные изменения",
4853
+ enabledBadge: "Включено",
4854
+ disabledBadge: "Выключено",
4855
+ syncNow: "Синхронизировать сейчас",
4856
+ syncing: "Синхронизирую…",
4857
+ enable: "Включить",
4858
+ disable: "Выключить",
4859
+ enabledDone: "Автоматическая синхронизация включена",
4860
+ disabledDone: "Автоматическая синхронизация выключена",
4861
+ openRepo: "Открыть репозиторий",
4862
+ howButton: "Как это работает",
4863
+ howShow: "Показать",
4864
+ howHide: "Свернуть",
4865
+ how1Title: "Приватный репозиторий",
4866
+ how1Text: "Один приватный репозиторий на вашем аккаунте GitHub хранит общую копию досье памяти проектов.",
4867
+ how2Title: "Трёхстороннее слияние",
4868
+ how2Text: "Каждая машина при синхронизации сравнивает «моя версия / прошлая общая / версия с GitHub» и отправляет результат обратно.",
4869
+ how3Title: "Ничего не затирается молча",
4870
+ how3Text: "Дополнения с разных АРМ объединяются. Если одна и та же строка изменена по-разному, появляется конфликт — и вы выбираете вариант.",
4871
+ sec1Title: "Только приватный репозиторий",
4872
+ sec1Text: "Плагин проверяет видимость и отказывается работать с публичным репозиторием.",
4873
+ sec2Title: "Секреты и PII вырезаются",
4874
+ sec2Text: "Чувствительные данные удаляются, домашние пути обезличиваются.",
4875
+ sec3Title: "Конфликты не перезаписывают файл",
4876
+ sec3Text: "Пока вы не выбрали вариант, локальный файл остаётся нетронутым.",
4877
+ sec4Title: "Бэкап и история Git",
4878
+ sec4Text: "Каждое изменение сохраняется с резервной копией и видно в истории Git.",
4879
+ optionsShow: "Настройки",
4880
+ optionsHide: "Скрыть настройки",
4881
+ optionsTitle: "Параметры синхронизации",
4882
+ autoSync: "Автоматическая синхронизация",
4883
+ autoSyncHint: "Синхронизировать при изменениях досье и по таймеру.",
4884
+ intervalLabel: "Интервал",
4885
+ secondsShort: "сек",
4886
+ excludeLabel: "Исключённые проекты",
4887
+ excludePlaceholder: "slug-один, slug-два",
4888
+ excludeHint: "Слаги проектов через запятую. Их досье не синхронизируются.",
4889
+ saveOptions: "Сохранить",
4890
+ saving: "Сохраняю…",
4891
+ optionsSaved: "Настройки сохранены",
4892
+ syncDone: "Синхронизация завершена",
4893
+ summaryProjects: "Проектов",
4894
+ summaryFiles: "Файлов",
4895
+ summaryConflicts: "Конфликтов",
4896
+ summaryPushed: "Изменения отправлены в GitHub",
4897
+ summaryLocalOnly: "Сохранено локально, отправка ещё впереди",
4898
+ conflictsTitle: "Что выбрать?",
4899
+ conflictsSubtitle: "Одна и та же запись изменилась здесь и на GitHub. Выберите, что оставить.",
4900
+ conflictMine: "Моё",
4901
+ conflictRemote: "С GitHub",
4902
+ conflictKeepMine: "Оставить моё",
4903
+ conflictTakeRemote: "Принять с GitHub",
4904
+ conflictMerge: "Объединить",
4905
+ hintKeepMine: "«Оставить моё» — отправит вашу версию в GitHub",
4906
+ hintTakeRemote: "«Принять с GitHub» — заменит этот файл версией из репозитория",
4907
+ hintMerge: "«Объединить» — сохранит оба варианта",
4908
+ conflictWorking: "Применяю…",
4909
+ conflictDone: "Конфликт разрешён",
4910
+ conflictFound: "Обнаружен",
4911
+ previewEmpty: "пусто",
4912
+ projectsEmpty: "Откройте папку проекта в DSH — досье появится здесь автоматически.",
4913
+ statusSynced: "Синхронизировано",
4914
+ statusLocalChanges: "Есть локальные правки",
4915
+ statusConflict: "Конфликт",
4916
+ statusNotOpened: "Не открыт локально",
4917
+ historyOpen: "История",
4918
+ historyTitle: "История версий",
4919
+ historySubtitle: "Commit’ы, которые меняли досье проекта. Восстановление создаёт новый commit — ничего не пропадает.",
4920
+ historyEmpty: "Версий пока нет — первая синхронизация их создаст.",
4921
+ historyLoading: "Загружаю историю…",
4922
+ historyClose: "Закрыть",
4923
+ historyRestore: "Восстановить",
4924
+ historyRestoreHint: "Восстановит файлы досье, которые были в этом commit’е; файлы, появившиеся позже, останутся. Текущие файлы сначала сохраняются в бэкап.",
4925
+ historyConfirm: "Восстановить досье проекта на эту версию? Текущие файлы будут сохранены в бэкап, затем создастся новый commit.",
4926
+ historyDone: "Версия восстановлена",
4927
+ historyAuthor: "автор",
4928
+ diffLegend: "Изменённые строки подсвечены: розовые — только у меня, зелёные — только на GitHub",
4929
+ portableTitle: "Перенос через флешку",
4930
+ portableHeading: "Забрать память проектов с собой",
4931
+ portableSubtitle: "Сохраните досье на флешку и восстановите на другой машине — без GitHub и команд.",
4932
+ portablePlaceholder: "E:\\ (флешка или папка)",
4933
+ portableScan: "Сканировать",
4934
+ portableExport: "Сохранить на флешку",
4935
+ portableImport: "Импортировать",
4936
+ portableHint: "На флешке — обезличенные Markdown-досье без секретов и AUTO-SCAN. Для чувствительных данных включите BitLocker на флешке.",
4937
+ portableEmpty: "На флешке пока нет папки dsh-memory-portable.",
4938
+ portableFound: "На флешке проектов: {count}",
4939
+ portableConfirm: "Импортировать проекты с флешки? Текущие досье сначала сохранятся в бэкап.",
4940
+ portableImported: "Импортировано проектов: {count}",
4941
+ portableExported: "Сохранено на флешку проектов: {count}",
4942
+ portableScanFirst: "Сначала сканируйте флешку.",
4943
+ portableNoPath: "Укажите путь к флешке.",
4944
+ portableNew: "новый",
4945
+ portableUpdate: "обновить",
4946
+ portableSame: "актуально",
4947
+ portableImportedState: "импортирован",
4948
+ portableImportedNew: "восстановлен",
4949
+ portableSkipped: "пропущен",
4950
+ portableOnlyMemory: "Только досье памяти",
4951
+ portableCross: "Исходники и рабочие папки переносятся отдельно: «Перенос и резервные копии → Проекты и код».",
4952
+ portableStepSave: "Сохранить",
4953
+ portableStepScan: "Сканировать",
4954
+ portableStepImport: "Восстановить",
4955
+ };
4956
+ const ZH = {
4957
+ nav: "项目记忆",
4958
+ title: "项目记忆",
4959
+ subtitle: "项目记忆档案通过你自己的私有 GitHub 仓库同步。无需命令——所有操作都在本页用按钮完成。",
4960
+ langProbe: "zh",
4961
+ loading: "正在读取同步状态…",
4962
+ loadError: "无法读取同步状态",
4963
+ retry: "重试",
4964
+ errorPrefix: "无法完成:",
4965
+ syncErrorLabel: "同步错误",
4966
+ never: "尚未执行",
4967
+ secRepo: "仓库",
4968
+ secSync: "同步",
4969
+ secHow: "工作原理",
4970
+ secSecurity: "安全",
4971
+ secConflicts: "冲突",
4972
+ secProjects: "项目",
4973
+ ghMissingTitle: "需要 GitHub CLI",
4974
+ ghMissingText: "插件通过 GitHub CLI 工作。请在本机安装并登录一次。",
4975
+ ghInstallStep: "安装 GitHub CLI",
4976
+ ghInstallHint: "从 cli.github.com 下载并安装 GitHub CLI(gh)。",
4977
+ ghLoginStep: "在终端中执行",
4978
+ ghLoginHint: "每台机器只需登录一次。",
4979
+ checkAgain: "再次检查",
4980
+ checking: "检查中…",
4981
+ ghLoginTitle: "登录 GitHub",
4982
+ ghLoginText: "GitHub CLI 已安装,但尚未登录。",
4983
+ repoSetupTitle: "连接仓库",
4984
+ repoSetupText: "档案将通过你自己的私有仓库同步。",
4985
+ ghLoggedIn: "GitHub:已登录为",
4986
+ accountHint: "仓库将创建在你的账号下——你的档案不会进入别人的仓库。",
4987
+ repoLabel: "仓库",
4988
+ repoPlaceholder: "owner/name",
4989
+ createRepo: "创建私有仓库",
4990
+ creatingRepo: "创建中…",
4991
+ connectExisting: "连接已有的私有仓库",
4992
+ connectingRepo: "连接中…",
4993
+ repoFieldHint: "owner/name,仅限 private;插件会检查可见性。",
4994
+ repoInvalid: "请以 owner/name 格式填写仓库。",
4995
+ privateOnlyNote: "插件拒绝使用公开仓库。",
4996
+ configureDone: "仓库已连接",
4997
+ createDone: "私有仓库已创建并连接",
4998
+ gitMissing: "本机未找到 Git。没有它同步无法推送修改。",
4999
+ ghNotAuthorizedWarn: "GitHub CLI 未授权——同步无法推送修改。",
5000
+ stateSynced: "全部已同步",
5001
+ stateLocalChanges: "有本地修改",
5002
+ conflictOne: "个冲突",
5003
+ conflictFew: "个冲突",
5004
+ conflictMany: "个冲突",
5005
+ visPrivate: "私有",
5006
+ visPublic: "公开 — 请停用!",
5007
+ visUnknown: "可见性未确认",
5008
+ publicRepoWarn: "仓库是公开的。请停用同步:项目记忆档案不应对外可见。",
5009
+ lastSync: "上次同步",
5010
+ branch: "分支",
5011
+ pendingLocal: "本地修改",
5012
+ pendingUnpushed: "有未推送的修改",
5013
+ enabledBadge: "已启用",
5014
+ disabledBadge: "已禁用",
5015
+ syncNow: "立即同步",
5016
+ syncing: "同步中…",
5017
+ enable: "启用",
5018
+ disable: "禁用",
5019
+ enabledDone: "自动同步已启用",
5020
+ disabledDone: "自动同步已禁用",
5021
+ openRepo: "打开仓库",
5022
+ howButton: "工作原理",
5023
+ howShow: "显示",
5024
+ howHide: "收起",
5025
+ how1Title: "私有仓库",
5026
+ how1Text: "你 GitHub 账号下的一个私有仓库保存项目记忆档案的共享副本。",
5027
+ how2Title: "三方合并",
5028
+ how2Text: "每次同步时,机器会比较「我的版本 / 上次共同版本 / GitHub 上的版本」,并把结果推送回去。",
5029
+ how3Title: "不会静默覆盖",
5030
+ how3Text: "来自不同工作站的补充会合并。如果同一行被改得不一样,就会出现冲突,由你选择保留哪个版本。",
5031
+ sec1Title: "仅限私有仓库",
5032
+ sec1Text: "插件检查可见性,拒绝使用公开仓库。",
5033
+ sec2Title: "敏感信息会被剔除",
5034
+ sec2Text: "敏感数据会被删除,本机路径会被匿名化。",
5035
+ sec3Title: "冲突不会覆盖文件",
5036
+ sec3Text: "在你选择版本之前,本地文件保持不变。",
5037
+ sec4Title: "备份与 Git 历史",
5038
+ sec4Text: "每次修改都会保留备份,并可在 Git 历史中查看。",
5039
+ optionsShow: "设置",
5040
+ optionsHide: "收起设置",
5041
+ optionsTitle: "同步设置",
5042
+ autoSync: "自动同步",
5043
+ autoSyncHint: "在档案变化时和按定时器同步。",
5044
+ intervalLabel: "间隔",
5045
+ secondsShort: "秒",
5046
+ excludeLabel: "排除的项目",
5047
+ excludePlaceholder: "slug-one, slug-two",
5048
+ excludeHint: "项目 slug,用逗号分隔。这些档案不会被同步。",
5049
+ saveOptions: "保存",
5050
+ saving: "保存中…",
5051
+ optionsSaved: "设置已保存",
5052
+ syncDone: "同步完成",
5053
+ summaryProjects: "项目",
5054
+ summaryFiles: "文件",
5055
+ summaryConflicts: "冲突",
5056
+ summaryPushed: "修改已推送到 GitHub",
5057
+ summaryLocalOnly: "已在本地保存,等待推送",
5058
+ conflictsTitle: "要选哪个?",
5059
+ conflictsSubtitle: "同一条记录在本机和 GitHub 上都发生了变化。请选择保留哪一份。",
5060
+ conflictMine: "我的",
5061
+ conflictRemote: "来自 GitHub",
5062
+ conflictKeepMine: "保留我的",
5063
+ conflictTakeRemote: "采用 GitHub 版本",
5064
+ conflictMerge: "合并",
5065
+ hintKeepMine: "「保留我的」——把你的版本发送到 GitHub",
5066
+ hintTakeRemote: "「采用 GitHub 版本」——用仓库版本替换此文件",
5067
+ hintMerge: "「合并」——保留两个版本",
5068
+ conflictWorking: "正在应用…",
5069
+ conflictDone: "冲突已解决",
5070
+ conflictFound: "发现于",
5071
+ previewEmpty: "空",
5072
+ projectsEmpty: "在 DSH 中打开项目文件夹——档案会自动出现在这里。",
5073
+ statusSynced: "已同步",
5074
+ statusLocalChanges: "有本地修改",
5075
+ statusConflict: "冲突",
5076
+ statusNotOpened: "本地未打开",
5077
+ historyOpen: "历史",
5078
+ historyTitle: "版本历史",
5079
+ historySubtitle: "修改此项目档案的提交。恢复会创建新提交,不会丢失任何内容。",
5080
+ historyEmpty: "还没有版本——首次同步后会创建。",
5081
+ historyLoading: "正在加载历史…",
5082
+ historyClose: "关闭",
5083
+ historyRestore: "恢复",
5084
+ historyRestoreHint: "恢复该提交中存在的档案文件;之后新增的文件会保留。恢复前会先备份当前文件。",
5085
+ historyConfirm: "将此项目档案恢复到该版本?当前文件会先备份,然后创建新提交。",
5086
+ historyDone: "版本已恢复",
5087
+ historyAuthor: "作者",
5088
+ diffLegend: "变更行已高亮:粉色—仅本地,绿色—仅 GitHub",
5089
+ portableTitle: "U 盘传输",
5090
+ portableHeading: "随身携带项目记忆",
5091
+ portableSubtitle: "将档案保存到 U 盘,在另一台机器上恢复——无需 GitHub 和命令。",
5092
+ portablePlaceholder: "E:\\(U 盘或文件夹)",
5093
+ portableScan: "扫描",
5094
+ portableExport: "保存到 U 盘",
5095
+ portableImport: "导入",
5096
+ portableHint: "U 盘内是已脱敏的 Markdown 档案,不含密钥和 AUTO-SCAN。敏感数据请启用 BitLocker。",
5097
+ portableEmpty: "U 盘上还没有 dsh-memory-portable 文件夹。",
5098
+ portableFound: "U 盘上有 {count} 个项目",
5099
+ portableConfirm: "从 U 盘导入项目?当前档案会先备份。",
5100
+ portableImported: "已导入 {count} 个项目",
5101
+ portableExported: "已保存 {count} 个项目到 U 盘",
5102
+ portableScanFirst: "请先扫描 U 盘。",
5103
+ portableNoPath: "请输入 U 盘路径。",
5104
+ portableNew: "新项目",
5105
+ portableUpdate: "可更新",
5106
+ portableSame: "已是最新",
5107
+ portableImportedState: "已导入",
5108
+ portableImportedNew: "已恢复",
5109
+ portableSkipped: "已跳过",
5110
+ portableOnlyMemory: "仅项目记忆档案",
5111
+ portableCross: "源码和工作区文件夹请使用「备份与传输 → 项目与代码」。",
5112
+ portableStepSave: "保存",
5113
+ portableStepScan: "扫描",
5114
+ portableStepImport: "恢复",
5115
+ };
5116
+
5117
+ const inject = ["slots", "locale", "connection"];
5118
+
5119
+ /* ------------------------------------------------------------------ */
5120
+ /* inline SVG icons — no emoji, only currentColor strokes */
5121
+ /* ------------------------------------------------------------------ */
5122
+
5123
+ const ICONS = {
5124
+ check: [["path", { d: "M20 6 9 17l-5-5" }]],
5125
+ alert: [["path", { d: "M12 3 22 21H2Z" }], ["path", { d: "M12 9v5" }], ["path", { d: "M12 17h.01" }]],
5126
+ sync: [
5127
+ ["path", { d: "M21 2v6h-6" }],
5128
+ ["path", { d: "M3 12a9 9 0 0 1 15-6.7L21 8" }],
5129
+ ["path", { d: "M3 22v-6h6" }],
5130
+ ["path", { d: "M21 12a9 9 0 0 1-15 6.7L3 16" }],
5131
+ ],
5132
+ power: [["path", { d: "M18.36 6.64a9 9 0 1 1-12.73 0" }], ["path", { d: "M12 2v10" }]],
5133
+ external: [
5134
+ ["path", { d: "M14 4h6v6" }],
5135
+ ["path", { d: "M20 4 11 13" }],
5136
+ ["path", { d: "M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" }],
5137
+ ],
5138
+ 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" }]],
5139
+ gear: [
5140
+ ["circle", { cx: "12", cy: "12", r: "3" }],
5141
+ ["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" }],
5142
+ ],
5143
+ chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
5144
+ chevronUp: [["path", { d: "m6 15 6-6 6 6" }]],
5145
+ terminal: [["rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }], ["path", { d: "m7 9 3 3-3 3" }], ["path", { d: "M13 15h4" }]],
5146
+ user: [["path", { d: "M20 21a8 8 0 0 0-16 0" }], ["circle", { cx: "12", cy: "7", r: "4" }]],
5147
+ 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" }]],
5148
+ lock: [["rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }], ["path", { d: "M8 11V8a4 4 0 0 1 8 0v3" }]],
5149
+ eyeOff: [
5150
+ ["path", { d: "m3 3 18 18" }],
5151
+ ["path", { d: "M10.6 10.6a2 2 0 0 0 2.8 2.8" }],
5152
+ ["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" }],
5153
+ ["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" }],
5154
+ ],
5155
+ 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" }]],
5156
+ history: [["path", { d: "M3 12a9 9 0 1 0 3-6.7" }], ["path", { d: "M3 4v5h5" }], ["path", { d: "M12 8v4l3 2" }]],
5157
+ plus: [["path", { d: "M12 5v14" }], ["path", { d: "M5 12h14" }]],
5158
+ 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" }]],
5159
+ clock: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M12 7v5l3 2" }]],
5160
+ dot: [["circle", { cx: "12", cy: "12", r: "3" }]],
5161
+ usb: [
5162
+ ["path", { d: "M12 3v11" }],
5163
+ ["path", { d: "m8 10 4 4 4-4" }],
5164
+ ["rect", { x: "7", y: "15", width: "10", height: "6", rx: "2" }],
5165
+ ["path", { d: "M9 3h6" }],
5166
+ ],
5167
+ save: [
5168
+ ["path", { d: "M4 4h12l4 4v12H4Z" }],
5169
+ ["path", { d: "M8 4v6h8V4" }],
5170
+ ["path", { d: "M8 20v-6h8v6" }],
5171
+ ],
5172
+ scan: [
5173
+ ["path", { d: "M4 8V5a1 1 0 0 1 1-1h3" }],
5174
+ ["path", { d: "M16 4h3a1 1 0 0 1 1 1v3" }],
5175
+ ["path", { d: "M20 16v3a1 1 0 0 1-1 1h-3" }],
5176
+ ["path", { d: "M8 20H5a1 1 0 0 1-1-1v-3" }],
5177
+ ["path", { d: "M4 12h16" }],
5178
+ ],
5179
+ };
5180
+
5181
+ function Icon(props) {
5182
+ const size = props.size ?? 16;
5183
+ const shapes = ICONS[props.name] ?? ICONS.dot;
5184
+ return h(
5185
+ "svg",
5186
+ {
5187
+ className: "dmb-icon",
5188
+ width: size,
5189
+ height: size,
5190
+ viewBox: "0 0 24 24",
5191
+ fill: "none",
5192
+ stroke: "currentColor",
5193
+ strokeWidth: 1.7,
5194
+ strokeLinecap: "round",
5195
+ strokeLinejoin: "round",
5196
+ "aria-hidden": "true",
5197
+ focusable: "false",
5198
+ },
5199
+ shapes.map(([tag, attrs], index) => h(tag, { key: index, ...attrs })),
5200
+ );
5201
+ }
5202
+
5203
+ function Spinner() {
5204
+ return h("span", { className: "dmb-spinner", "aria-hidden": "true" });
5205
+ }
5206
+
5207
+ function Banner(props) {
5208
+ const tone = props.tone ?? "ok";
5209
+ const iconName = props.icon ?? (tone === "ok" ? "check" : tone === "info" ? "shield" : "alert");
5210
+ return h(
5211
+ "div",
5212
+ { className: "dmb-status", "data-tone": tone, role: tone === "error" ? "alert" : "status" },
5213
+ h(Icon, { name: iconName, size: 15 }),
5214
+ h("span", { className: "dmb-status-text" }, props.children),
5215
+ );
5216
+ }
5217
+
5218
+ function Chip(props) {
5219
+ return h(
5220
+ "span",
5221
+ { className: "dmb-chip", "data-tone": props.tone ?? "muted" },
5222
+ props.icon !== undefined && props.icon !== null ? h(Icon, { name: props.icon, size: 12 }) : null,
5223
+ h("span", null, props.children),
5224
+ );
5225
+ }
5226
+
5227
+ function Eyebrow(props) {
5228
+ return h(
5229
+ "div",
5230
+ { className: "dmb-eyebrow" },
5231
+ h("span", { className: "dmb-eyebrow-num" }, props.number),
5232
+ h("span", { className: "dmb-eyebrow-label" }, props.label),
5233
+ );
5234
+ }
5235
+
5236
+ function CardHead(props) {
5237
+ return h(
5238
+ "div",
5239
+ { className: "dmb-card-head" },
5240
+ h(
5241
+ "div",
5242
+ { className: "dmb-card-head-main" },
5243
+ h(Eyebrow, { number: props.number, label: props.label }),
5244
+ props.title !== undefined && props.title !== null ? h("div", { className: "dmb-card-title" }, props.title) : null,
5245
+ props.sub !== undefined && props.sub !== null ? h("div", { className: "dmb-card-sub" }, props.sub) : null,
5246
+ ),
5247
+ props.right !== undefined && props.right !== null ? props.right : null,
5248
+ );
5249
+ }
5250
+
5251
+ function KV(props) {
5252
+ return h("div", { className: "dmb-kv" }, h("span", { className: "dmb-kv-label" }, props.label), h("span", { className: "dmb-kv-value" }, props.children));
5253
+ }
5254
+
5255
+ /* ------------------------------------------------------------------ */
5256
+ /* pure helpers */
5257
+ /* ------------------------------------------------------------------ */
5258
+
5259
+ function errorText(error) {
5260
+ if (error === undefined || error === null) return "unknown error";
5261
+ if (typeof error === "string") return error;
5262
+ if (typeof error.message === "string" && error.message.length > 0) return error.message;
5263
+ if (error.error && typeof error.error.message === "string" && error.error.message.length > 0) return error.error.message;
5264
+ try {
5265
+ return JSON.stringify(error);
5266
+ } catch {
5267
+ return String(error);
5268
+ }
5269
+ }
5270
+
5271
+ /** The Connection carrier resolves envelopes: {ok:true, value} or {ok:false, error}. */
5272
+ function assertOk(result) {
5273
+ if (result !== null && typeof result === "object") {
5274
+ if (result.ok === false) throw new Error(errorText(result.error ?? result.message));
5275
+ if (result.value !== null && typeof result.value === "object" && result.value.ok === false) {
5276
+ throw new Error(errorText(result.value.error ?? result.value.message));
5277
+ }
5278
+ }
5279
+ return result;
5280
+ }
5281
+
5282
+ function looksLikeStatus(value) {
5283
+ if (value === null || typeof value !== "object") return false;
5284
+ return typeof value.configured === "boolean" || Array.isArray(value.projects) || Array.isArray(value.conflicts);
5285
+ }
5286
+
5287
+ function statusFrom(result) {
5288
+ if (result === null || typeof result !== "object") return null;
5289
+ if (result.status !== null && typeof result.status === "object" && looksLikeStatus(result.status)) return result.status;
5290
+ if (looksLikeStatus(result)) return result;
5291
+ if (result.value !== null && typeof result.value === "object") {
5292
+ if (result.value.status !== null && typeof result.value.status === "object" && looksLikeStatus(result.value.status)) return result.value.status;
5293
+ if (looksLikeStatus(result.value)) return result.value;
5294
+ }
5295
+ return null;
5296
+ }
5297
+
5298
+ function historyFrom(result) {
5299
+ if (result === null || typeof result !== "object") return [];
5300
+ if (Array.isArray(result.commits)) return result.commits;
5301
+ if (result.value !== null && typeof result.value === "object" && Array.isArray(result.value.commits)) return result.value.commits;
5302
+ return [];
5303
+ }
5304
+
5305
+ /** Line-level diff for the two conflict previews (pink = only mine, green = only GitHub). */
5306
+ function diffRows(text, other) {
5307
+ const mine = String(text ?? "").split("\n");
5308
+ const theirs = String(other ?? "").split("\n");
5309
+ const n = mine.length;
5310
+ const m = theirs.length;
5311
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
5312
+ for (let i = n - 1; i >= 0; i -= 1) {
5313
+ for (let j = m - 1; j >= 0; j -= 1) {
5314
+ dp[i][j] = mine[i] === theirs[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
5315
+ }
5316
+ }
5317
+ const rows = [];
5318
+ let i = 0;
5319
+ let j = 0;
5320
+ while (i < n && j < m) {
5321
+ if (mine[i] === theirs[j]) {
5322
+ rows.push({ text: mine[i], changed: false });
5323
+ i += 1;
5324
+ j += 1;
5325
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
5326
+ rows.push({ text: mine[i], changed: true });
5327
+ i += 1;
5328
+ } else {
5329
+ j += 1;
5330
+ }
5331
+ }
5332
+ while (i < n) {
5333
+ rows.push({ text: mine[i], changed: true });
5334
+ i += 1;
5335
+ }
5336
+ return rows;
5337
+ }
5338
+
5339
+ function formatTime(value) {
5340
+ if (typeof value !== "string" || value.length === 0) return null;
5341
+ const date = new Date(value);
5342
+ if (Number.isNaN(date.getTime())) return value;
5343
+ try {
5344
+ return date.toLocaleString(undefined, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
5345
+ } catch {
5346
+ return value;
5347
+ }
5348
+ }
5349
+
5350
+ /** “5 минут назад” — localised through Intl.RelativeTimeFormat. */
5351
+ function relativeTime(value, lang) {
5352
+ if (typeof value !== "string" || value.length === 0) return null;
5353
+ const date = new Date(value);
5354
+ if (Number.isNaN(date.getTime())) return value;
5355
+ const diffSeconds = Math.round((date.getTime() - Date.now()) / 1000);
5356
+ const abs = Math.abs(diffSeconds);
5357
+ let unit = "second";
5358
+ let divisor = 1;
5359
+ if (abs >= 31536000) {
5360
+ unit = "year";
5361
+ divisor = 31536000;
5362
+ } else if (abs >= 2592000) {
5363
+ unit = "month";
5364
+ divisor = 2592000;
5365
+ } else if (abs >= 86400) {
5366
+ unit = "day";
5367
+ divisor = 86400;
5368
+ } else if (abs >= 3600) {
5369
+ unit = "hour";
5370
+ divisor = 3600;
5371
+ } else if (abs >= 60) {
5372
+ unit = "minute";
5373
+ divisor = 60;
5374
+ }
5375
+ try {
5376
+ return new Intl.RelativeTimeFormat(lang, { numeric: "auto" }).format(Math.round(diffSeconds / divisor), unit);
5377
+ } catch {
5378
+ return formatTime(value) ?? value;
5379
+ }
5380
+ }
5381
+
5382
+ function parseSlugs(value) {
5383
+ const seen = Object.create(null);
5384
+ const result = [];
5385
+ String(value ?? "").split(/[,;\s]+/u).forEach((raw) => {
5386
+ const slug = raw.trim().toLowerCase();
5387
+ if (slug.length === 0) return;
5388
+ if (seen[slug] === true) return;
5389
+ seen[slug] = true;
5390
+ result.push(slug);
5391
+ });
5392
+ return result;
5393
+ }
5394
+
5395
+ function optionsFromStatus(value) {
5396
+ const options = value !== null && typeof value === "object" && value.options !== null && typeof value.options === "object" ? value.options : {};
5397
+ const interval = Number(options.intervalSeconds);
5398
+ return {
5399
+ autoSync: options.autoSync === true,
5400
+ intervalSeconds: ALLOWED_INTERVALS.includes(interval) ? String(interval) : "300",
5401
+ excludeProjects: Array.isArray(options.excludeProjects) ? options.excludeProjects.join(", ") : "",
5402
+ };
5403
+ }
5404
+
5405
+ function emptyDraft() {
5406
+ return { autoSync: true, intervalSeconds: "300", excludeProjects: "" };
5407
+ }
5408
+
5409
+ /** Suggested repo comes from the Host: `<login>/dsh-memory-exchange`, never hard-coded. */
5410
+ function suggestedRepoFrom(status) {
5411
+ if (status === null || typeof status !== "object") return null;
5412
+ const github = status.github !== null && typeof status.github === "object" ? status.github : null;
5413
+ const repo =
5414
+ github !== null && typeof github.suggestedRepo === "string"
5415
+ ? github.suggestedRepo
5416
+ : typeof status.suggestedRepo === "string"
5417
+ ? status.suggestedRepo
5418
+ : "";
5419
+ return repo.length > 0 ? repo : null;
5420
+ }
5421
+
5422
+ function githubFrom(status) {
5423
+ const raw = status !== null && typeof status === "object" && status.github !== null && typeof status.github === "object" ? status.github : null;
5424
+ return {
5425
+ available: raw !== null && raw.available === true,
5426
+ authenticated: raw !== null && raw.authenticated === true,
5427
+ login: raw !== null && typeof raw.login === "string" && raw.login.length > 0 ? raw.login : null,
5428
+ suggestedRepo: suggestedRepoFrom(status),
5429
+ };
5430
+ }
5431
+
5432
+ /** "private" | "public" | "unknown" — Host sends `repoVisibility`/`visibility`. */
5433
+ function visibilityFrom(status) {
5434
+ if (status === null || typeof status !== "object") return "unknown";
5435
+ const repo = status.repo !== null && typeof status.repo === "object" ? status.repo : null;
5436
+ const github = status.github !== null && typeof status.github === "object" ? status.github : null;
5437
+ const candidates = [
5438
+ status.repoVisibility,
5439
+ status.visibility,
5440
+ github !== null ? github.repoVisibility : null,
5441
+ status.repository !== null && typeof status.repository === "object" ? status.repository.visibility : null,
5442
+ status.repoInfo !== null && typeof status.repoInfo === "object" ? status.repoInfo.visibility : null,
5443
+ repo !== null ? repo.visibility : null,
5444
+ ];
5445
+ for (const candidate of candidates) {
5446
+ const value = String(candidate ?? "").toLowerCase();
5447
+ if (value === "private") return "private";
5448
+ if (value === "public") return "public";
5449
+ }
5450
+ if (status.private === true || status.isPrivate === true || (repo !== null && repo.private === true)) return "private";
5451
+ if (status.private === false || status.isPrivate === false || (repo !== null && repo.private === false)) return "public";
5452
+ return "unknown";
5453
+ }
5454
+
5455
+ const PROJECT_STATUS_KEY = {
5456
+ synced: "statusSynced",
5457
+ "local-changes": "statusLocalChanges",
5458
+ conflict: "statusConflict",
5459
+ "not-opened": "statusNotOpened",
5460
+ };
5461
+
5462
+ function projectStatusKey(value) {
5463
+ return PROJECT_STATUS_KEY[String(value ?? "")] ?? "statusNotOpened";
5464
+ }
5465
+
5466
+ function projectStatusTone(value) {
5467
+ if (value === "synced") return "ok";
5468
+ if (value === "local-changes") return "warn";
5469
+ if (value === "conflict") return "error";
5470
+ return "muted";
5471
+ }
5472
+
5473
+ /* ------------------------------------------------------------------ */
5474
+ /* plugin */
5475
+ /* ------------------------------------------------------------------ */
5476
+
5477
+ function apply(ctx) {
5478
+ const t = ctx.locale.bind(NS);
5479
+
5480
+ const callbacks = {
5481
+ call: async (operation, payload) => {
5482
+ let signal;
5483
+ try {
5484
+ signal = AbortSignal.timeout(180000);
5485
+ } catch {
5486
+ signal = undefined;
5487
+ }
5488
+ return await ctx.connection.rpc.call("/api", `${RPC_NS}/${operation}`, payload ?? {}, signal);
5489
+ },
5490
+ openUrl: (url) => {
5491
+ try {
5492
+ window.open(url, "_blank", "noopener,noreferrer");
5493
+ return true;
5494
+ } catch {
5495
+ return false;
5496
+ }
5497
+ },
5498
+ };
5499
+
5500
+ ctx.effect(() => ctx.locale.register(NS, { en: EN, zh: ZH }), "memory-bridge: dictionaries");
5501
+ ctx.effect(() => {
5502
+ try {
5503
+ return ctx.locale.register(NS, "ru", RU);
5504
+ } catch {
5505
+ return () => {};
5506
+ }
5507
+ }, "memory-bridge: ru dictionary");
5508
+
5509
+ ctx.effect(() => {
5510
+ const style = document.createElement("style");
5511
+ style.id = "dsh-memory-bridge-style";
5512
+ style.textContent = `
5513
+ .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)}
5514
+ .dmb-head{display:flex;flex-direction:column;gap:6px}
5515
+ .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)}
5516
+ .dmb-kicker::before{content:"";width:28px;height:1px;background:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
5517
+ .dmb-title{font-size:22px;line-height:28px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
5518
+ .dmb-sub{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,#5b6472);max-width:66ch}
5519
+ .dmb-banners{display:flex;flex-direction:column;gap:8px}
5520
+ .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}
5521
+ .dmb-card--flat{gap:12px;padding:16px 18px}
5522
+ .dmb-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
5523
+ .dmb-card-head-main{display:flex;flex-direction:column;gap:4px;min-width:0}
5524
+ .dmb-card-title{font-size:16px;line-height:22px;font-weight:650;color:var(--dsw-alias-label-primary,#111);margin-top:2px}
5525
+ .dmb-card-sub{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472);max-width:62ch}
5526
+ .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)}
5527
+ .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}
5528
+ .dmb-icon{flex:none;display:block}
5529
+ .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}
5530
+ .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)}
5531
+ .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)}
5532
+ .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}
5533
+ .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)}
5534
+ .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}
5535
+ .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)}
5536
+ .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)}
5537
+ .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)}
5538
+ .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)}
5539
+ .dmb-state-body{display:flex;flex-direction:column;gap:6px;min-width:0}
5540
+ .dmb-state-title{font-size:18px;line-height:24px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
5541
+ .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)}
5542
+ .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)}
5543
+ .dmb-sep{color:var(--dsw-alias-border-l2,rgba(0,0,0,.24))}
5544
+ .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}
5545
+ .dmb-link:hover{text-decoration:underline}
5546
+ .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)}
5547
+ .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)}
5548
+ .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)}
5549
+ .dmb-account .dmb-icon{color:var(--dsw-alias-brand-primary,#2f7df6)}
5550
+ .dmb-note{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#5b6472)}
5551
+ .dmb-note .dmb-icon{margin-top:1px}
5552
+ .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}
5553
+ .dmb-field{display:flex;flex-direction:column;gap:6px}
5554
+ .dmb-label{font-size:11.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#6b7280)}
5555
+ .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}
5556
+ .dmb-portable-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0}
5557
+ .dmb-portable-row .dmb-input{flex:1;min-width:220px}
5558
+ .dmb-portable-row .dmb-btn{flex:none}
5559
+ .dmb-portable-projects{margin-top:10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));padding-top:8px}
5560
+ .dmb-portable-hint{margin-top:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
5561
+ .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)))}
5562
+ .dmb-usb .dmb-card-head{flex-wrap:wrap;gap:8px}
5563
+ .dmb-usb .dmb-card-head-main{min-width:0}
5564
+ .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}
5565
+ .dmb-usb-steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:2px 0 2px}
5566
+ .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))}
5567
+ .dmb-usb-step-icon{display:inline-flex;color:var(--dsw-alias-brand-primary,#2f7df6)}
5568
+ .dmb-usb-step-label{font-size:11px;line-height:14px;font-weight:650;color:var(--dsw-alias-label-secondary,#5b6472)}
5569
+ .dmb-usb-step-arrow{margin-left:2px;color:var(--dsw-alias-label-secondary,#9aa0a9);font-size:11px}
5570
+ .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}
5571
+ .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)}
5572
+ .dmb-usb-field-icon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary,#6b7280)}
5573
+ .dmb-usb-field .dmb-input{border:none;background:transparent;padding-left:0;box-shadow:none;outline:none}
5574
+ .dmb-usb-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
5575
+ .dmb-usb-actions .dmb-btn{min-height:32px;padding:0 12px;font-size:12px}
5576
+ .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)}
5577
+ .dmb-usb-cross .dmb-icon{flex:none;margin-top:1px;color:var(--dsw-alias-label-secondary,#8a9098)}
5578
+ @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%}}
5579
+ .dmb-input--mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:17px}
5580
+ .dmb-input:focus-visible,.dmb-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
5581
+ .dmb-input:disabled,.dmb-select:disabled{opacity:.6}
5582
+ .dmb-hint{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary,#6b7280)}
5583
+ .dmb-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
5584
+ .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}
5585
+ .dmb-btn:hover:not([disabled]){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
5586
+ .dmb-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
5587
+ .dmb-btn[data-tone="primary"]{background:var(--dsw-alias-label-primary,#111);border-color:transparent;color:var(--dsw-alias-bg-base,#fff)}
5588
+ .dmb-btn[data-tone="primary"]:hover:not([disabled]){filter:brightness(1.06);background:var(--dsw-alias-label-primary,#111)}
5589
+ .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)}
5590
+ .dmb-btn[data-tone="caution"]:hover:not([disabled]){background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 8%,transparent)}
5591
+ .dmb-btn--ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary,#5b6472);padding:6px 10px;min-height:30px;font-size:12px}
5592
+ .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)}
5593
+ .dmb-btn--small{padding:6px 10px;min-height:30px;font-size:12px}
5594
+ .dmb-btn[disabled]{opacity:.55;cursor:default}
5595
+ .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}
5596
+ @keyframes dmb-spin{to{transform:rotate(360deg)}}
5597
+ .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}
5598
+ .dmb-status .dmb-icon{margin-top:1px}
5599
+ .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)}
5600
+ .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)}
5601
+ .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)}
5602
+ .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)}
5603
+ .dmb-status-text{min-width:0}
5604
+ .dmb-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
5605
+ .dmb-step{display:flex;gap:12px;align-items:flex-start}
5606
+ .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}
5607
+ .dmb-step-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
5608
+ .dmb-step-title{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
5609
+ .dmb-step-text{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#5b6472)}
5610
+ .dmb-security{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:12px}
5611
+ .dmb-security li{display:flex;gap:10px;align-items:flex-start;min-width:0}
5612
+ .dmb-security-icon{color:var(--dsw-alias-label-secondary,#6b7280);margin-top:1px}
5613
+ .dmb-security-title{font-size:12.5px;line-height:17px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
5614
+ .dmb-security-text{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary,#5b6472)}
5615
+ .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}
5616
+ .dmb-conflict-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
5617
+ .dmb-conflict-title{font-size:13.5px;line-height:19px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
5618
+ .dmb-path{margin:0}
5619
+ .dmb-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
5620
+ .dmb-grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
5621
+ .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}
5622
+ .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}
5623
+ .dmb-diff-line{display:block;min-height:1.15em}
5624
+ .dmb-diff-del{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 16%,transparent);border-radius:3px}
5625
+ .dmb-diff-add{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 16%,transparent);border-radius:3px}
5626
+ .dmb-btn--sm{min-height:28px;padding:0 10px;font-size:12px;border-radius:8px;font-weight:600}
5627
+ .dmb-diff-legend{margin-top:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
5628
+ .dmb-kv{display:flex;flex-direction:column;gap:2px;min-width:0}
5629
+ .dmb-kv-label{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
5630
+ .dmb-kv-value{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums}
5631
+ .dmb-hints{display:flex;flex-direction:column;gap:4px}
5632
+ .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)}
5633
+ .dmb-hint-line .dmb-icon{margin-top:1px;color:var(--dsw-alias-label-secondary,#6b7280)}
5634
+ .dmb-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
5635
+ .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))}
5636
+ .dmb-row:last-child{border-bottom:none}
5637
+ .dmb-row-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
5638
+ .dmb-row-title{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
5639
+ .dmb-row-desc{font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary,#6b7280)}
5640
+ .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}
5641
+ .dmb-empty{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);padding:6px 0}
5642
+ .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}
5643
+ .dmb-divider{height:1px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.08));margin:2px 0}
5644
+ .dmb-options{display:flex;flex-direction:column;gap:14px}
5645
+ .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}
5646
+ .dmb-check input{margin-top:1px;accent-color:var(--dsw-alias-brand-primary,#2f7df6);width:15px;height:15px;flex:none}
5647
+ .dmb-check-text{display:flex;flex-direction:column;gap:2px;min-width:0}
5648
+ .dmb-check-title{font-weight:600}
5649
+ @media (max-width:560px){
5650
+ .dmb-page{gap:14px}
5651
+ .dmb-card{padding:14px}
5652
+ .dmb-grid2,.dmb-grid3,.dmb-security{grid-template-columns:1fr}
5653
+ .dmb-actions{flex-direction:column;align-items:stretch}
5654
+ .dmb-actions .dmb-btn{width:100%}
5655
+ .dmb-card-head{flex-direction:column;align-items:flex-start;gap:8px}
5656
+ .dmb-row{flex-wrap:wrap;align-items:flex-start}
5657
+ .dmb-row-text{flex:1 1 100%}
5658
+ .dmb-row .dmb-chip{order:1}
5659
+ .dmb-row-time{order:2;margin-left:auto}
5660
+ .dmb-row .dmb-btn{order:3;flex:1 1 100%;margin-top:4px}
5661
+ .dmb-row-title,.dmb-row-desc{overflow-wrap:break-word;word-break:normal}
5662
+ .dmb-state-title{font-size:16px;line-height:22px}
5663
+ }
5664
+ @media (prefers-reduced-motion:reduce){.dmb-spinner{animation-duration:2.4s}}
5665
+ `;
5666
+ document.head.appendChild(style);
5667
+ return () => style.remove();
5668
+ }, "memory-bridge: styles");
5669
+
5670
+ const call = callbacks.call;
5671
+ const openUrl = callbacks.openUrl;
5672
+
5673
+ function MemorySyncSection(props) {
5674
+ const rpc = props.call ?? call;
5675
+ const [phase, setPhase] = useState("loading");
5676
+ const [status, setStatus] = useState(null);
5677
+ const [busy, setBusy] = useState(null);
5678
+ const [message, setMessage] = useState(null);
5679
+ const [summary, setSummary] = useState(null);
5680
+ const [repoInput, setRepoInput] = useState("");
5681
+ const [optionsOpen, setOptionsOpen] = useState(false);
5682
+ const [draft, setDraft] = useState(emptyDraft);
5683
+ const [howOpen, setHowOpen] = useState(false);
5684
+ const [historyFor, setHistoryFor] = useState(null);
5685
+ const [historyItems, setHistoryItems] = useState(null);
5686
+ const [historyLoading, setHistoryLoading] = useState(false);
5687
+ const [portablePath, setPortablePath] = useState("");
5688
+ const [portableBusy, setPortableBusy] = useState(null);
5689
+ const [portableResult, setPortableResult] = useState(null);
5690
+ const [portableMessage, setPortableMessage] = useState(null);
5691
+ const repoEditedRef = useRef(false);
5692
+ const lastSuggestedRef = useRef(null);
5693
+ const aliveRef = useRef(true);
5694
+
5695
+ const lang = (() => {
5696
+ const value = t("langProbe");
5697
+ return typeof value === "string" && value.length > 0 ? value : "en";
5698
+ })();
5699
+
5700
+ function pluralWord(count, oneKey, fewKey, manyKey) {
5701
+ const n = Number(count) || 0;
5702
+ let word;
5703
+ if (lang === "ru") {
5704
+ const mod10 = n % 10;
5705
+ const mod100 = n % 100;
5706
+ if (mod10 === 1 && mod100 !== 11) word = t(oneKey);
5707
+ else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) word = t(fewKey);
5708
+ else word = t(manyKey);
5709
+ } else {
5710
+ word = n === 1 ? t(oneKey) : t(manyKey);
5711
+ }
5712
+ return `${n} ${word}`;
5713
+ }
5714
+
5715
+ async function loadStatus(silent) {
5716
+ try {
5717
+ const result = assertOk(await rpc("status", {}));
5718
+ if (aliveRef.current !== true) return false;
5719
+ const next = statusFrom(result);
5720
+ if (next === null) {
5721
+ if (silent !== true) {
5722
+ setPhase("error");
5723
+ setMessage({ tone: "error", text: t("loadError") });
5724
+ }
5725
+ return false;
5726
+ }
5727
+ setStatus(next);
5728
+ setPhase("ready");
5729
+ const suggested = suggestedRepoFrom(next);
5730
+ if (repoEditedRef.current !== true) {
5731
+ setRepoInput((current) => {
5732
+ if (current.length === 0) return suggested ?? "";
5733
+ if (lastSuggestedRef.current !== null && current === lastSuggestedRef.current) return suggested ?? current;
5734
+ return current;
5735
+ });
5736
+ }
5737
+ if (suggested !== null) lastSuggestedRef.current = suggested;
5738
+ return true;
5739
+ } catch (error) {
5740
+ if (aliveRef.current !== true) return false;
5741
+ setPhase((current) => (current === "ready" ? current : "error"));
5742
+ if (silent !== true) setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5743
+ return false;
5744
+ }
5745
+ }
5746
+
5747
+ useEffect(() => {
5748
+ aliveRef.current = true;
5749
+ void loadStatus(false);
5750
+ const timer = setInterval(() => {
5751
+ void loadStatus(true);
5752
+ }, 10000);
5753
+ return () => {
5754
+ aliveRef.current = false;
5755
+ clearInterval(timer);
5756
+ };
5757
+ }, []);
5758
+
5759
+ function takeStatus(result) {
5760
+ const next = statusFrom(result);
5761
+ if (next !== null) {
5762
+ setStatus(next);
5763
+ setPhase("ready");
5764
+ }
5765
+ return next;
5766
+ }
5767
+
5768
+ function toggleOptions() {
5769
+ if (optionsOpen === true) {
5770
+ setOptionsOpen(false);
5771
+ return;
5772
+ }
5773
+ setDraft(optionsFromStatus(status));
5774
+ setOptionsOpen(true);
5775
+ }
5776
+
5777
+ function toggleHow() {
5778
+ setHowOpen((current) => current !== true);
5779
+ }
5780
+
5781
+ function openRepo() {
5782
+ const url = status !== null && typeof status === "object" && typeof status.repoUrl === "string" ? status.repoUrl : "";
5783
+ if (url.length === 0) return;
5784
+ openUrl(url);
5785
+ }
5786
+
5787
+ async function doCheck() {
5788
+ if (busy !== null) return;
5789
+ setBusy("check");
5790
+ setMessage(null);
5791
+ try {
5792
+ await loadStatus(false);
5793
+ } finally {
5794
+ setBusy(null);
5795
+ }
5796
+ }
5797
+
5798
+ async function doConnect(kind) {
5799
+ if (busy !== null) return;
5800
+ const repo = repoInput.trim();
5801
+ if (kind === "configure" && repo.length === 0) {
5802
+ setMessage({ tone: "error", text: t("repoInvalid") });
5803
+ return;
5804
+ }
5805
+ if (repo.length > 0 && REPO_PATTERN.test(repo) !== true) {
5806
+ setMessage({ tone: "error", text: t("repoInvalid") });
5807
+ return;
5808
+ }
5809
+ const busyKey = kind === "create" ? "create" : "configure";
5810
+ setBusy(busyKey);
5811
+ setMessage(null);
5812
+ setSummary(null);
5813
+ try {
5814
+ const payload = kind === "create" ? (repo.length > 0 ? { repo } : {}) : { repo };
5815
+ const result = assertOk(await rpc(kind === "create" ? "create-repo" : "configure", payload));
5816
+ const next = takeStatus(result);
5817
+ if (next !== null) setDraft(optionsFromStatus(next));
5818
+ const created =
5819
+ result !== null && typeof result === "object" && result.value !== null && typeof result.value === "object" ? result.value.created !== false : true;
5820
+ setMessage({ tone: "ok", text: kind === "create" ? (created === true ? t("createDone") : t("configureDone")) : t("configureDone") });
5821
+ await loadStatus(true);
5822
+ } catch (error) {
5823
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5824
+ } finally {
5825
+ setBusy(null);
5826
+ }
5827
+ }
5828
+
5829
+ async function doSync() {
5830
+ if (busy !== null) return;
5831
+ setBusy("sync");
5832
+ setMessage(null);
5833
+ setSummary(null);
5834
+ try {
5835
+ const result = assertOk(await rpc("sync-now", {}));
5836
+ takeStatus(result);
5837
+ setSummary(summaryFrom(result));
5838
+ setMessage({ tone: "ok", text: t("syncDone") });
5839
+ await loadStatus(true);
5840
+ } catch (error) {
5841
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5842
+ await loadStatus(true);
5843
+ } finally {
5844
+ setBusy(null);
5845
+ }
5846
+ }
5847
+
5848
+ async function doToggle() {
5849
+ if (busy !== null || status === null) return;
5850
+ const enable = status.enabled !== true;
5851
+ setBusy("toggle");
5852
+ setMessage(null);
5853
+ setSummary(null);
5854
+ try {
5855
+ const result = assertOk(await rpc("set-enabled", { enabled: enable }));
5856
+ takeStatus(result);
5857
+ setMessage({ tone: "ok", text: enable === true ? t("enabledDone") : t("disabledDone") });
5858
+ await loadStatus(true);
5859
+ } catch (error) {
5860
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5861
+ } finally {
5862
+ setBusy(null);
5863
+ }
5864
+ }
5865
+
5866
+ async function doSaveOptions() {
5867
+ if (busy !== null) return;
5868
+ const interval = Number(draft.intervalSeconds);
5869
+ setBusy("options");
5870
+ setMessage(null);
5871
+ setSummary(null);
5872
+ try {
5873
+ const result = assertOk(await rpc("set-options", {
5874
+ autoSync: draft.autoSync === true,
5875
+ intervalSeconds: ALLOWED_INTERVALS.includes(interval) ? interval : 300,
5876
+ excludeProjects: parseSlugs(draft.excludeProjects),
5877
+ }));
5878
+ const next = takeStatus(result);
5879
+ if (next !== null) setDraft(optionsFromStatus(next));
5880
+ setMessage({ tone: "ok", text: t("optionsSaved") });
5881
+ await loadStatus(true);
5882
+ } catch (error) {
5883
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5884
+ } finally {
5885
+ setBusy(null);
5886
+ }
5887
+ }
5888
+
5889
+ async function doResolve(conflictId, choice) {
5890
+ if (busy !== null) return;
5891
+ setBusy(`conflict:${conflictId}:${choice}`);
5892
+ setMessage(null);
5893
+ setSummary(null);
5894
+ try {
5895
+ const result = assertOk(await rpc("resolve-conflict", { id: conflictId, choice }));
5896
+ takeStatus(result);
5897
+ setMessage({ tone: "ok", text: t("conflictDone") });
5898
+ await loadStatus(true);
5899
+ } catch (error) {
5900
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5901
+ } finally {
5902
+ setBusy(null);
5903
+ }
5904
+ }
5905
+
5906
+ /* ---------------- USB portable transfer ---------------- */
5907
+
5908
+ function retry() {
5909
+ setMessage(null);
5910
+ setPhase("loading");
5911
+ void loadStatus(false);
5912
+ }
5913
+
5914
+ function portablePayload(result) {
5915
+ if (result !== null && typeof result === "object" && result.value !== null && typeof result.value === "object") {
5916
+ return result.value;
5917
+ }
5918
+ return result;
5919
+ }
5920
+
5921
+ async function runPortable(action) {
5922
+ if (portableBusy !== null) return;
5923
+ const source = portablePath.trim();
5924
+ if (source.length === 0) {
5925
+ setPortableMessage({ tone: "error", text: t("portableNoPath") });
5926
+ return;
5927
+ }
5928
+ if (action === "import") {
5929
+ const confirmed =
5930
+ typeof window === "undefined" || typeof window.confirm !== "function" ? true : window.confirm(t("portableConfirm"));
5931
+ if (!confirmed) return;
5932
+ }
5933
+ setPortableBusy(action);
5934
+ setPortableMessage(null);
5935
+ try {
5936
+ const method = action === "scan" ? "portable-scan" : action === "export" ? "portable-export" : "portable-import";
5937
+ const payload = action === "export" ? { destination: source } : { source };
5938
+ const result = assertOk(await rpc(method, payload));
5939
+ const value = portablePayload(result);
5940
+ setPortableResult(value);
5941
+ if (action === "scan") {
5942
+ const count = Array.isArray(value?.projects) ? value.projects.length : 0;
5943
+ setPortableMessage({ tone: count > 0 ? "ok" : "error", text: count > 0 ? formatTemplate(t("portableFound"), { count }) : t("portableEmpty") });
5944
+ } else if (action === "export") {
5945
+ setPortableMessage({ tone: "ok", text: formatTemplate(t("portableExported"), { count: Number(value?.projects?.length) || 0 }) });
5946
+ } else {
5947
+ setPortableMessage({ tone: "ok", text: formatTemplate(t("portableImported"), { count: Number(value?.imported) || 0 }) });
5948
+ takeStatus(result);
5949
+ await loadStatus(true);
5950
+ }
5951
+ } catch (error) {
5952
+ setPortableMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5953
+ } finally {
5954
+ setPortableBusy(null);
5955
+ }
5956
+ }
5957
+
5958
+ /* ---------------- history and restore ---------------- */
5959
+
5960
+ async function openHistory(slug) {
5961
+ if (busy !== null) return;
5962
+ setHistoryFor(slug);
5963
+ setHistoryItems(null);
5964
+ setHistoryLoading(true);
5965
+ setMessage(null);
5966
+ try {
5967
+ const result = assertOk(await rpc("history", { slug }));
5968
+ setHistoryItems(historyFrom(result));
5969
+ } catch (error) {
5970
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5971
+ setHistoryFor(null);
5972
+ } finally {
5973
+ setHistoryLoading(false);
5974
+ }
5975
+ }
5976
+
5977
+ function closeHistory() {
5978
+ setHistoryFor(null);
5979
+ setHistoryItems(null);
5980
+ setHistoryLoading(false);
5981
+ }
5982
+
5983
+ async function doRestore(slug, commit) {
5984
+ if (busy !== null) return;
5985
+ const confirmed =
5986
+ typeof window === "undefined" || typeof window.confirm !== "function" ? true : window.confirm(t("historyConfirm"));
5987
+ if (!confirmed) return;
5988
+ setBusy(`restore:${slug}`);
5989
+ setMessage(null);
5990
+ setSummary(null);
5991
+ try {
5992
+ const result = assertOk(await rpc("restore-version", { slug, commit }));
5993
+ takeStatus(result);
5994
+ setMessage({ tone: "ok", text: t("historyDone") });
5995
+ closeHistory();
5996
+ await loadStatus(true);
5997
+ } catch (error) {
5998
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${errorText(error)}` });
5999
+ } finally {
6000
+ setBusy(null);
6001
+ }
6002
+ }
6003
+
6004
+ /* ---------------- derived view state ---------------- */
6005
+
6006
+ const configured = status !== null && typeof status === "object" && status.configured === true;
6007
+ const github = githubFrom(status);
6008
+ const conflicts = status !== null && status !== undefined && Array.isArray(status.conflicts) ? status.conflicts : [];
6009
+ const projects = status !== null && status !== undefined && Array.isArray(status.projects) ? status.projects : [];
6010
+ const pending = status !== null && typeof status === "object" && status.pending !== null && typeof status.pending === "object" ? status.pending : {};
6011
+ const localChanges = Number(pending.localChanges) || 0;
6012
+ const unpushed = pending.unpushed === true;
6013
+ const conflictCount = conflicts.length;
6014
+ const repoVisibilityRaw = visibilityFrom(status);
6015
+ const repoVisibility = configured === true ? repoVisibilityRaw : "unknown";
6016
+ const publicRepo = repoVisibility === "public";
6017
+ const gitAvailable = status === null || typeof status !== "object" ? true : status.gitAvailable !== false;
6018
+ const lastSyncText = status !== null && typeof status === "object" ? relativeTime(status.lastSyncAt, lang) : null;
6019
+ const repoUrl = status !== null && typeof status === "object" && typeof status.repoUrl === "string" ? status.repoUrl : "";
6020
+ const branch = status !== null && typeof status === "object" && typeof status.branch === "string" && status.branch.length > 0 ? status.branch : "main";
6021
+ const repoName = status !== null && typeof status === "object" && typeof status.repo === "string" ? status.repo : "";
6022
+
6023
+ let stateTone;
6024
+ let stateText;
6025
+ if (conflictCount > 0) {
6026
+ stateTone = "error";
6027
+ stateText = pluralWord(conflictCount, "conflictOne", "conflictFew", "conflictMany");
6028
+ } else if (localChanges > 0 || unpushed) {
6029
+ stateTone = "warn";
6030
+ stateText = t("stateLocalChanges");
6031
+ } else {
6032
+ stateTone = "ok";
6033
+ stateText = t("stateSynced");
6034
+ }
6035
+
6036
+ let sectionCounter = 0;
6037
+ const nextSectionNumber = () => String((sectionCounter += 1)).padStart(2, "0");
6038
+ const numberMain = nextSectionNumber();
6039
+ const numberHow = nextSectionNumber();
6040
+ const numberSecurity = nextSectionNumber();
6041
+ const numberConflicts = conflictCount > 0 ? nextSectionNumber() : null;
6042
+ const numberProjects = nextSectionNumber();
6043
+ const numberHistory = nextSectionNumber();
6044
+ const numberPortable = nextSectionNumber();
6045
+
6046
+ const head = h(
6047
+ "div",
6048
+ { className: "dmb-head" },
6049
+ h("div", { className: "dmb-kicker" }, "DeepSeek Harness"),
6050
+ h("div", { className: "dmb-title" }, t("title")),
6051
+ h("div", { className: "dmb-sub" }, t("subtitle")),
6052
+ );
6053
+
6054
+ const banners = [];
6055
+ if (status !== null && typeof status === "object" && typeof status.lastError === "string" && status.lastError.length > 0) {
6056
+ banners.push(h(Banner, { key: "last-error", tone: "error" }, `${t("syncErrorLabel")}: ${status.lastError}`));
6057
+ }
6058
+ if (message !== null && !(phase === "error" && status === null)) {
6059
+ banners.push(h(Banner, { key: "message", tone: message.tone ?? "ok" }, message.text));
6060
+ }
6061
+ const bannersBlock = banners.length > 0 ? h("div", { className: "dmb-banners" }, banners) : null;
6062
+
6063
+ const loadingCard = h(
6064
+ "section",
6065
+ { className: "dmb-card", "aria-busy": "true" },
6066
+ h("div", { className: "dmb-loading" }, h(Spinner), h("span", null, t("loading"))),
6067
+ );
6068
+
6069
+ const errorCard = h(
6070
+ "section",
6071
+ { className: "dmb-card" },
6072
+ h(Banner, { tone: "error" }, message !== null ? message.text : t("loadError")),
6073
+ h("div", { className: "dmb-actions" }, h("button", { type: "button", className: "dmb-btn", onClick: retry }, h(Icon, { name: "sync" }), t("retry"))),
6074
+ );
6075
+
6076
+ /* ---------------- setup cards ---------------- */
6077
+
6078
+ const ghCommand = () => h("code", { className: "dmb-code" }, "gh auth login");
6079
+
6080
+ const ghMissingCard = h(
6081
+ "section",
6082
+ { className: "dmb-card", "aria-label": t("secRepo") },
6083
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("ghMissingTitle"), sub: t("ghMissingText") }),
6084
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
6085
+ h(
6086
+ "ol",
6087
+ { className: "dmb-steps" },
6088
+ h(
6089
+ "li",
6090
+ { className: "dmb-step", key: "install" },
6091
+ h("span", { className: "dmb-step-num" }, "1"),
6092
+ h(
6093
+ "div",
6094
+ { className: "dmb-step-body" },
6095
+ h("div", { className: "dmb-step-title" }, t("ghInstallStep")),
6096
+ h("div", { className: "dmb-step-text" }, t("ghInstallHint")),
6097
+ ),
6098
+ ),
6099
+ h(
6100
+ "li",
6101
+ { className: "dmb-step", key: "login" },
6102
+ h("span", { className: "dmb-step-num" }, "2"),
6103
+ h(
6104
+ "div",
6105
+ { className: "dmb-step-body" },
6106
+ h("div", { className: "dmb-step-title" }, t("ghLoginStep")),
6107
+ ghCommand(),
6108
+ h("div", { className: "dmb-step-text" }, t("ghLoginHint")),
6109
+ ),
6110
+ ),
6111
+ ),
6112
+ h(
6113
+ "div",
6114
+ { className: "dmb-actions" },
6115
+ h(
6116
+ "button",
6117
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doCheck() },
6118
+ busy === "check" ? h(Spinner) : h(Icon, { name: "terminal" }),
6119
+ busy === "check" ? t("checking") : t("checkAgain"),
6120
+ ),
6121
+ ),
6122
+ );
6123
+
6124
+ const ghLoginCard = h(
6125
+ "section",
6126
+ { className: "dmb-card", "aria-label": t("secRepo") },
6127
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("ghLoginTitle"), sub: t("ghLoginText") }),
6128
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
6129
+ h("div", { className: "dmb-step-title" }, t("ghLoginStep")),
6130
+ ghCommand(),
6131
+ h("div", { className: "dmb-hint" }, t("ghLoginHint")),
6132
+ h(
6133
+ "div",
6134
+ { className: "dmb-actions" },
6135
+ h(
6136
+ "button",
6137
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doCheck() },
6138
+ busy === "check" ? h(Spinner) : h(Icon, { name: "terminal" }),
6139
+ busy === "check" ? t("checking") : t("checkAgain"),
6140
+ ),
6141
+ ),
6142
+ );
6143
+
6144
+ const repoSetupCard = h(
6145
+ "section",
6146
+ { className: "dmb-card", "aria-label": t("secRepo") },
6147
+ h(CardHead, { number: numberMain, label: t("secRepo"), title: t("repoSetupTitle"), sub: t("repoSetupText") }),
6148
+ h(
6149
+ "div",
6150
+ { className: "dmb-account" },
6151
+ h(Icon, { name: "user", size: 15 }),
6152
+ h("span", null, t("ghLoggedIn")),
6153
+ h("b", null, github.login ?? "—"),
6154
+ ),
6155
+ h("div", { className: "dmb-note" }, h(Icon, { name: "shield", size: 15 }), h("span", null, t("accountHint"))),
6156
+ h(
6157
+ "div",
6158
+ { className: "dmb-field" },
6159
+ h("label", { className: "dmb-label", htmlFor: "dmb-repo" }, t("repoLabel")),
6160
+ h("input", {
6161
+ id: "dmb-repo",
6162
+ className: "dmb-input dmb-input--mono",
6163
+ type: "text",
6164
+ value: repoInput,
6165
+ placeholder: t("repoPlaceholder"),
6166
+ spellCheck: false,
6167
+ autoComplete: "off",
6168
+ disabled: busy !== null,
6169
+ onChange: (event) => {
6170
+ repoEditedRef.current = true;
6171
+ setRepoInput(event.target.value);
6172
+ },
6173
+ onKeyDown: (event) => {
6174
+ if (event.key === "Enter") {
6175
+ event.preventDefault();
6176
+ void doConnect("create");
6177
+ }
6178
+ },
6179
+ }),
6180
+ h("div", { className: "dmb-hint" }, t("repoFieldHint")),
6181
+ ),
6182
+ h(
6183
+ "div",
6184
+ { className: "dmb-actions" },
6185
+ h(
6186
+ "button",
6187
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doConnect("create") },
6188
+ busy === "create" ? h(Spinner) : h(Icon, { name: "plus" }),
6189
+ busy === "create" ? t("creatingRepo") : t("createRepo"),
6190
+ ),
6191
+ h(
6192
+ "button",
6193
+ { type: "button", className: "dmb-btn", disabled: busy !== null, onClick: () => void doConnect("configure") },
6194
+ busy === "configure" ? h(Spinner) : h(Icon, { name: "repo" }),
6195
+ busy === "configure" ? t("connectingRepo") : t("connectExisting"),
6196
+ ),
6197
+ ),
6198
+ h("div", { className: "dmb-note" }, h(Icon, { name: "lock", size: 15 }), h("span", null, t("privateOnlyNote"))),
6199
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
6200
+ );
6201
+
6202
+ /* ---------------- connected status card ---------------- */
6203
+
6204
+ const visibilityChip =
6205
+ repoVisibility === "private"
6206
+ ? h(Chip, { tone: "ok", icon: "lock" }, t("visPrivate"))
6207
+ : repoVisibility === "public"
6208
+ ? h(Chip, { tone: "error", icon: "alert" }, t("visPublic"))
6209
+ : h(Chip, { tone: "muted", icon: "eyeOff" }, t("visUnknown"));
6210
+
6211
+ const stateBlock = h(
6212
+ "div",
6213
+ { className: "dmb-state", "data-tone": stateTone },
6214
+ h("span", { className: "dmb-state-icon" }, h(Icon, { name: stateTone === "ok" ? "check" : "alert", size: 18 })),
6215
+ h(
6216
+ "div",
6217
+ { className: "dmb-state-body" },
6218
+ h("div", { className: "dmb-state-title" }, stateText),
6219
+ h(
6220
+ "div",
6221
+ { className: "dmb-state-meta" },
6222
+ visibilityChip,
6223
+ github.login !== null ? h("span", null, `${t("ghLoggedIn")} `, h("b", null, github.login)) : null,
6224
+ h("span", null, `${t("lastSync")}: `, h("span", null, lastSyncText ?? t("never"))),
6225
+ localChanges > 0 ? h(Chip, { tone: "warn" }, `${t("pendingLocal")}: ${localChanges}`) : null,
6226
+ unpushed === true ? h(Chip, { tone: "warn" }, t("pendingUnpushed")) : null,
6227
+ ),
6228
+ ),
6229
+ );
6230
+
6231
+ const repoLine = h(
6232
+ "div",
6233
+ { className: "dmb-repo-line" },
6234
+ repoUrl.length > 0
6235
+ ? h("a", { className: "dmb-link", href: repoUrl, target: "_blank", rel: "noopener noreferrer" }, repoName.length > 0 ? repoName : repoUrl)
6236
+ : h("span", { className: "dmb-mono" }, repoName.length > 0 ? repoName : "—"),
6237
+ h("span", { className: "dmb-sep", "aria-hidden": "true" }, "·"),
6238
+ h("span", null, `${t("branch")}: `, h("span", { className: "dmb-mono" }, branch)),
6239
+ );
6240
+
6241
+ const summaryBlock =
6242
+ summary !== null
6243
+ ? h(
6244
+ "div",
6245
+ { className: "dmb-preview" },
6246
+ h(
6247
+ "div",
6248
+ { className: "dmb-grid3" },
6249
+ h(KV, { label: t("summaryProjects") }, String(summary.projects ?? 0)),
6250
+ h(KV, { label: t("summaryFiles") }, String(summary.files ?? 0)),
6251
+ h(KV, { label: t("summaryConflicts") }, String(summary.conflicts ?? 0)),
6252
+ ),
6253
+ h("div", { className: "dmb-hint" }, summary.pushed === true ? t("summaryPushed") : t("summaryLocalOnly")),
6254
+ )
6255
+ : null;
6256
+
6257
+ const optionsPanel =
6258
+ optionsOpen === true
6259
+ ? h(
6260
+ "div",
6261
+ { className: "dmb-options" },
6262
+ h("div", { className: "dmb-card-title" }, t("optionsTitle")),
6263
+ h(
6264
+ "label",
6265
+ { className: "dmb-check" },
6266
+ h("input", {
6267
+ type: "checkbox",
6268
+ checked: draft.autoSync === true,
6269
+ disabled: busy !== null,
6270
+ onChange: (event) => setDraft((current) => ({ ...current, autoSync: event.target.checked })),
6271
+ }),
6272
+ h(
6273
+ "span",
6274
+ { className: "dmb-check-text" },
6275
+ h("span", { className: "dmb-check-title" }, t("autoSync")),
6276
+ h("span", { className: "dmb-hint" }, t("autoSyncHint")),
6277
+ ),
6278
+ ),
6279
+ h(
6280
+ "div",
6281
+ { className: "dmb-field" },
6282
+ h("label", { className: "dmb-label", htmlFor: "dmb-interval" }, t("intervalLabel")),
6283
+ h(
6284
+ "select",
6285
+ {
6286
+ id: "dmb-interval",
6287
+ className: "dmb-select",
6288
+ value: draft.intervalSeconds,
6289
+ disabled: busy !== null,
6290
+ onChange: (event) => setDraft((current) => ({ ...current, intervalSeconds: event.target.value })),
6291
+ },
6292
+ ALLOWED_INTERVALS.map((seconds) => h("option", { key: String(seconds), value: String(seconds) }, `${seconds} ${t("secondsShort")}`)),
6293
+ ),
6294
+ ),
6295
+ h(
6296
+ "div",
6297
+ { className: "dmb-field" },
6298
+ h("label", { className: "dmb-label", htmlFor: "dmb-exclude" }, t("excludeLabel")),
6299
+ h("input", {
6300
+ id: "dmb-exclude",
6301
+ className: "dmb-input dmb-input--mono",
6302
+ type: "text",
6303
+ value: draft.excludeProjects,
6304
+ placeholder: t("excludePlaceholder"),
6305
+ spellCheck: false,
6306
+ autoComplete: "off",
6307
+ disabled: busy !== null,
6308
+ onChange: (event) => setDraft((current) => ({ ...current, excludeProjects: event.target.value })),
6309
+ }),
6310
+ h("div", { className: "dmb-hint" }, t("excludeHint")),
6311
+ ),
6312
+ h(
6313
+ "div",
6314
+ { className: "dmb-actions" },
6315
+ h(
6316
+ "button",
6317
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null, onClick: () => void doSaveOptions() },
6318
+ busy === "options" ? h(Spinner) : h(Icon, { name: "check" }),
6319
+ busy === "options" ? t("saving") : t("saveOptions"),
6320
+ ),
6321
+ ),
6322
+ )
6323
+ : null;
6324
+
6325
+ const statusCard = h(
6326
+ "section",
6327
+ { className: "dmb-card", "aria-label": t("secSync") },
6328
+ h(
6329
+ "div",
6330
+ { className: "dmb-card-head" },
6331
+ h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberMain, label: t("secSync") })),
6332
+ h(Chip, { tone: status?.enabled === true ? "ok" : "muted", icon: status?.enabled === true ? "check" : "power" }, status?.enabled === true ? t("enabledBadge") : t("disabledBadge")),
6333
+ ),
6334
+ stateBlock,
6335
+ repoLine,
6336
+ publicRepo === true ? h(Banner, { tone: "error" }, t("publicRepoWarn")) : null,
6337
+ gitAvailable === false ? h(Banner, { tone: "warn" }, t("gitMissing")) : null,
6338
+ github.authenticated !== true ? h(Banner, { tone: "warn" }, t("ghNotAuthorizedWarn")) : null,
6339
+ h(
6340
+ "div",
6341
+ { className: "dmb-actions" },
6342
+ h(
6343
+ "button",
6344
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: busy !== null || publicRepo === true, onClick: () => void doSync() },
6345
+ busy === "sync" ? h(Spinner) : h(Icon, { name: "sync" }),
6346
+ busy === "sync" ? t("syncing") : t("syncNow"),
6347
+ ),
6348
+ h(
6349
+ "button",
6350
+ { type: "button", className: "dmb-btn", disabled: busy !== null, onClick: () => void doToggle() },
6351
+ busy === "toggle" ? h(Spinner) : h(Icon, { name: "power" }),
6352
+ status?.enabled === true ? t("disable") : t("enable"),
6353
+ ),
6354
+ h(
6355
+ "button",
6356
+ { type: "button", className: "dmb-btn", disabled: busy !== null || repoUrl.length === 0, onClick: openRepo },
6357
+ h(Icon, { name: "external" }),
6358
+ t("openRepo"),
6359
+ ),
6360
+ h(
6361
+ "button",
6362
+ { type: "button", className: "dmb-btn", "aria-expanded": howOpen === true, onClick: toggleHow },
6363
+ h(Icon, { name: "help" }),
6364
+ t("howButton"),
6365
+ ),
6366
+ h(
6367
+ "button",
6368
+ { type: "button", className: "dmb-btn", "aria-expanded": optionsOpen === true, onClick: toggleOptions },
6369
+ h(Icon, { name: "gear" }),
6370
+ optionsOpen === true ? t("optionsHide") : t("optionsShow"),
6371
+ ),
6372
+ ),
6373
+ summaryBlock,
6374
+ optionsPanel !== null ? h("div", { className: "dmb-divider" }) : null,
6375
+ optionsPanel,
6376
+ );
6377
+
6378
+ /* ---------------- how it works ---------------- */
6379
+
6380
+ const howCard = h(
6381
+ "section",
6382
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secHow") },
6383
+ h(
6384
+ "div",
6385
+ { className: "dmb-card-head" },
6386
+ h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberHow, label: t("secHow") })),
6387
+ h(
6388
+ "button",
6389
+ { type: "button", className: "dmb-btn dmb-btn--ghost dmb-btn--small", "aria-expanded": howOpen === true, onClick: toggleHow },
6390
+ h(Icon, { name: howOpen === true ? "chevronUp" : "chevronDown" }),
6391
+ howOpen === true ? t("howHide") : t("howShow"),
6392
+ ),
6393
+ ),
6394
+ howOpen === true
6395
+ ? h(
6396
+ "ol",
6397
+ { className: "dmb-steps" },
6398
+ h(
6399
+ "li",
6400
+ { className: "dmb-step", key: "how1" },
6401
+ h("span", { className: "dmb-step-num" }, "01"),
6402
+ h(
6403
+ "div",
6404
+ { className: "dmb-step-body" },
6405
+ h("div", { className: "dmb-step-title" }, t("how1Title")),
6406
+ h("div", { className: "dmb-step-text" }, t("how1Text")),
6407
+ ),
6408
+ ),
6409
+ h(
6410
+ "li",
6411
+ { className: "dmb-step", key: "how2" },
6412
+ h("span", { className: "dmb-step-num" }, "02"),
6413
+ h(
6414
+ "div",
6415
+ { className: "dmb-step-body" },
6416
+ h("div", { className: "dmb-step-title" }, t("how2Title")),
6417
+ h("div", { className: "dmb-step-text" }, t("how2Text")),
6418
+ ),
6419
+ ),
6420
+ h(
6421
+ "li",
6422
+ { className: "dmb-step", key: "how3" },
6423
+ h("span", { className: "dmb-step-num" }, "03"),
6424
+ h(
6425
+ "div",
6426
+ { className: "dmb-step-body" },
6427
+ h("div", { className: "dmb-step-title" }, t("how3Title")),
6428
+ h("div", { className: "dmb-step-text" }, t("how3Text")),
6429
+ ),
6430
+ ),
6431
+ )
6432
+ : null,
6433
+ );
6434
+
6435
+ /* ---------------- security ---------------- */
6436
+
6437
+ const securityCard = h(
6438
+ "section",
6439
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secSecurity") },
6440
+ h("div", { className: "dmb-card-head" }, h("div", { className: "dmb-card-head-main" }, h(Eyebrow, { number: numberSecurity, label: t("secSecurity") }))),
6441
+ h(
6442
+ "ul",
6443
+ { className: "dmb-security" },
6444
+ h(
6445
+ "li",
6446
+ { key: "sec1" },
6447
+ h(Icon, { name: "lock", size: 15 }),
6448
+ h(
6449
+ "div",
6450
+ null,
6451
+ h("div", { className: "dmb-security-title" }, t("sec1Title")),
6452
+ h("div", { className: "dmb-security-text" }, t("sec1Text")),
6453
+ ),
6454
+ ),
6455
+ h(
6456
+ "li",
6457
+ { key: "sec2" },
6458
+ h(Icon, { name: "eyeOff", size: 15 }),
6459
+ h(
6460
+ "div",
6461
+ null,
6462
+ h("div", { className: "dmb-security-title" }, t("sec2Title")),
6463
+ h("div", { className: "dmb-security-text" }, t("sec2Text")),
6464
+ ),
6465
+ ),
6466
+ h(
6467
+ "li",
6468
+ { key: "sec3" },
6469
+ h(Icon, { name: "file", size: 15 }),
6470
+ h(
6471
+ "div",
6472
+ null,
6473
+ h("div", { className: "dmb-security-title" }, t("sec3Title")),
6474
+ h("div", { className: "dmb-security-text" }, t("sec3Text")),
6475
+ ),
6476
+ ),
6477
+ h(
6478
+ "li",
6479
+ { key: "sec4" },
6480
+ h(Icon, { name: "history", size: 15 }),
6481
+ h(
6482
+ "div",
6483
+ null,
6484
+ h("div", { className: "dmb-security-title" }, t("sec4Title")),
6485
+ h("div", { className: "dmb-security-text" }, t("sec4Text")),
6486
+ ),
6487
+ ),
6488
+ ),
6489
+ );
6490
+
6491
+ /* ---------------- conflicts ---------------- */
6492
+
6493
+ function renderDiffPane(text, otherText, changedClass) {
6494
+ const rows = diffRows(text, otherText);
6495
+ return h(
6496
+ "pre",
6497
+ { className: "dmb-pre dmb-diff" },
6498
+ rows.map((row, index) =>
6499
+ h(
6500
+ "span",
6501
+ { key: index, className: `dmb-diff-line${row.changed ? ` ${changedClass}` : ""}` },
6502
+ row.text.length > 0 ? row.text : " ",
6503
+ ),
6504
+ ),
6505
+ );
6506
+ }
6507
+
6508
+ function renderConflict(conflict, index) {
6509
+ const id = String(conflict?.id ?? index);
6510
+ const path = typeof conflict?.path === "string" ? conflict.path : "";
6511
+ const projectName = conflict?.projectName || conflict?.projectSlug || id;
6512
+ const localPreview = typeof conflict?.localPreview === "string" && conflict.localPreview.length > 0 ? conflict.localPreview : t("previewEmpty");
6513
+ const remotePreview = typeof conflict?.remotePreview === "string" && conflict.remotePreview.length > 0 ? conflict.remotePreview : t("previewEmpty");
6514
+ const found = formatTime(conflict?.createdAt);
6515
+ const isWorking = (choice) => busy === `conflict:${id}:${choice}`;
6516
+ const disabled = busy !== null;
6517
+ return h(
6518
+ "div",
6519
+ { className: "dmb-conflict", key: id },
6520
+ h(
6521
+ "div",
6522
+ { className: "dmb-conflict-head" },
6523
+ h("div", { className: "dmb-conflict-title" }, projectName),
6524
+ found !== null ? h("span", { className: "dmb-hint" }, `${t("conflictFound")}: ${found}`) : null,
6525
+ ),
6526
+ path.length > 0 ? h("div", { className: "dmb-mono dmb-path" }, path) : null,
6527
+ h(
6528
+ "div",
6529
+ { className: "dmb-grid2" },
6530
+ h(
6531
+ "div",
6532
+ { className: "dmb-preview" },
6533
+ h("div", { className: "dmb-label" }, t("conflictMine")),
6534
+ renderDiffPane(localPreview, remotePreview, "dmb-diff-del"),
6535
+ ),
6536
+ h(
6537
+ "div",
6538
+ { className: "dmb-preview" },
6539
+ h("div", { className: "dmb-label" }, t("conflictRemote")),
6540
+ renderDiffPane(remotePreview, localPreview, "dmb-diff-add"),
6541
+ ),
6542
+ ),
6543
+ h("div", { className: "dmb-diff-legend" }, t("diffLegend")),
6544
+ h(
6545
+ "div",
6546
+ { className: "dmb-actions" },
6547
+ h(
6548
+ "button",
6549
+ { type: "button", className: "dmb-btn", disabled, onClick: () => void doResolve(id, "local") },
6550
+ isWorking("local") ? h(Spinner) : null,
6551
+ isWorking("local") ? t("conflictWorking") : t("conflictKeepMine"),
6552
+ ),
6553
+ h(
6554
+ "button",
6555
+ { type: "button", className: "dmb-btn", "data-tone": "caution", disabled, onClick: () => void doResolve(id, "remote") },
6556
+ isWorking("remote") ? h(Spinner) : null,
6557
+ isWorking("remote") ? t("conflictWorking") : t("conflictTakeRemote"),
6558
+ ),
6559
+ h(
6560
+ "button",
6561
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled, onClick: () => void doResolve(id, "both") },
6562
+ isWorking("both") ? h(Spinner) : null,
6563
+ isWorking("both") ? t("conflictWorking") : t("conflictMerge"),
6564
+ ),
6565
+ ),
6566
+ h(
6567
+ "div",
6568
+ { className: "dmb-hints" },
6569
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "chevronUp", size: 12 }), h("span", null, t("hintKeepMine"))),
6570
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "chevronDown", size: 12 }), h("span", null, t("hintTakeRemote"))),
6571
+ h("div", { className: "dmb-hint-line" }, h(Icon, { name: "check", size: 12 }), h("span", null, t("hintMerge"))),
6572
+ ),
6573
+ );
6574
+ }
6575
+
6576
+ const conflictsCard =
6577
+ conflictCount > 0
6578
+ ? h(
6579
+ "section",
6580
+ { className: "dmb-card", "aria-label": t("secConflicts") },
6581
+ h(CardHead, {
6582
+ number: numberConflicts,
6583
+ label: t("secConflicts"),
6584
+ title: t("conflictsTitle"),
6585
+ sub: t("conflictsSubtitle"),
6586
+ right: h(Chip, { tone: "error" }, String(conflictCount)),
6587
+ }),
6588
+ conflicts.map(renderConflict),
6589
+ )
6590
+ : null;
6591
+
6592
+ /* ---------------- projects ---------------- */
6593
+
6594
+ const projectsList =
6595
+ projects.length === 0
6596
+ ? h("div", { className: "dmb-empty" }, t("projectsEmpty"))
6597
+ : h(
6598
+ "div",
6599
+ { className: "dmb-list" },
6600
+ projects.map((project, index) =>
6601
+ h(
6602
+ "div",
6603
+ { className: "dmb-row", key: String(project?.slug ?? index) },
6604
+ h(
6605
+ "div",
6606
+ { className: "dmb-row-text" },
6607
+ h("div", { className: "dmb-row-title" }, project?.name || project?.slug || "—"),
6608
+ typeof project?.slug === "string" && project.slug.length > 0 ? h("div", { className: "dmb-row-desc dmb-mono" }, project.slug) : null,
6609
+ ),
6610
+ h(Chip, { tone: projectStatusTone(project?.status) }, t(projectStatusKey(project?.status))),
6611
+ h("div", { className: "dmb-row-time" }, relativeTime(project?.lastSyncAt, lang) ?? "—"),
6612
+ h(
6613
+ "button",
6614
+ {
6615
+ type: "button",
6616
+ className: "dmb-btn dmb-btn--sm",
6617
+ disabled: busy !== null,
6618
+ onClick: () => void openHistory(String(project?.slug ?? "")),
6619
+ },
6620
+ t("historyOpen"),
6621
+ ),
6622
+ ),
6623
+ ),
6624
+ );
6625
+
6626
+ const projectsCard = h(
6627
+ "section",
6628
+ { className: "dmb-card dmb-card--flat", "aria-label": t("secProjects") },
6629
+ h(CardHead, {
6630
+ number: numberProjects,
6631
+ label: t("secProjects"),
6632
+ right: h(Chip, { tone: "muted" }, String(projects.length)),
6633
+ }),
6634
+ projectsList,
6635
+ );
6636
+
6637
+ /* ---------------- history ---------------- */
6638
+
6639
+ const historyProject =
6640
+ historyFor === null ? null : projects.find((project) => String(project?.slug ?? "") === historyFor) ?? null;
6641
+ const historyCard =
6642
+ historyFor === null
6643
+ ? null
6644
+ : h(
6645
+ "section",
6646
+ { className: "dmb-card", "aria-label": t("historyTitle") },
6647
+ h(CardHead, {
6648
+ number: numberHistory,
6649
+ label: t("historyTitle"),
6650
+ title: historyProject?.name || historyFor,
6651
+ sub: t("historySubtitle"),
6652
+ right: h(
6653
+ "button",
6654
+ { type: "button", className: "dmb-btn dmb-btn--sm", disabled: busy !== null, onClick: closeHistory },
6655
+ t("historyClose"),
6656
+ ),
6657
+ }),
6658
+ historyLoading
6659
+ ? h("div", { className: "dmb-empty" }, t("historyLoading"))
6660
+ : historyItems === null || historyItems.length === 0
6661
+ ? h("div", { className: "dmb-empty" }, t("historyEmpty"))
6662
+ : h(
6663
+ "div",
6664
+ { className: "dmb-list" },
6665
+ historyItems.map((item, index) => {
6666
+ const sha = String(item?.sha ?? "");
6667
+ return h(
6668
+ "div",
6669
+ { className: "dmb-row", key: sha || String(index) },
6670
+ h(
6671
+ "div",
6672
+ { className: "dmb-row-text" },
6673
+ h("div", { className: "dmb-row-title" }, item?.subject || sha.slice(0, 8) || "—"),
6674
+ h(
6675
+ "div",
6676
+ { className: "dmb-row-desc" },
6677
+ [formatTime(item?.date) ?? item?.date ?? "", item?.author ? `${t("historyAuthor")} ${item.author}` : ""]
6678
+ .filter((part) => typeof part === "string" && part.length > 0)
6679
+ .join(" · "),
6680
+ ),
6681
+ ),
6682
+ h(
6683
+ "button",
6684
+ {
6685
+ type: "button",
6686
+ className: "dmb-btn dmb-btn--sm",
6687
+ disabled: busy !== null,
6688
+ onClick: () => void doRestore(historyFor, sha),
6689
+ },
6690
+ busy === `restore:${historyFor}` ? h(Spinner) : t("historyRestore"),
6691
+ ),
6692
+ );
6693
+ }),
6694
+ ),
6695
+ h("div", { className: "dmb-hint" }, t("historyRestoreHint")),
6696
+ );
6697
+
6698
+ /* ---------------- portable USB ---------------- */
6699
+
6700
+ const portableProjects =
6701
+ portableResult !== null && typeof portableResult === "object" && Array.isArray(portableResult.projects)
6702
+ ? portableResult.projects
6703
+ : [];
6704
+ const portableStatusKey = (value) => {
6705
+ const map = {
6706
+ new: "portableNew",
6707
+ update: "portableUpdate",
6708
+ same: "portableSame",
6709
+ imported: "portableImportedState",
6710
+ "imported-new": "portableImportedNew",
6711
+ skipped: "portableSkipped",
6712
+ };
6713
+ return map[String(value ?? "")] ?? "portableSame";
6714
+ };
6715
+ const portableStatusTone = (value) =>
6716
+ value === "same" || value === "imported" || value === "imported-new"
6717
+ ? "ok"
6718
+ : value === "update"
6719
+ ? "warn"
6720
+ : value === "new"
6721
+ ? "muted"
6722
+ : "muted";
6723
+ const portableCard =
6724
+ status === null
6725
+ ? null
6726
+ : h(
6727
+ "section",
6728
+ { className: "dmb-card dmb-usb", "aria-label": t("portableTitle") },
6729
+ h(CardHead, {
6730
+ number: numberPortable,
6731
+ label: t("portableTitle"),
6732
+ title: t("portableHeading"),
6733
+ sub: t("portableSubtitle"),
6734
+ right: h(
6735
+ "span",
6736
+ { className: "dmb-usb-badge" },
6737
+ h(Icon, { name: "usb", size: 12 }),
6738
+ t("portableOnlyMemory"),
6739
+ ),
6740
+ }),
6741
+ h(
6742
+ "div",
6743
+ { className: "dmb-usb-steps", "aria-hidden": "true" },
6744
+ [
6745
+ ["save", "portableStepSave"],
6746
+ ["scan", "portableStepScan"],
6747
+ ["check", "portableStepImport"],
6748
+ ].map(([icon, key], index) =>
6749
+ h(
6750
+ "div",
6751
+ { className: "dmb-usb-step", key },
6752
+ h("span", { className: "dmb-usb-step-icon" }, h(Icon, { name: icon, size: 13 })),
6753
+ h("span", { className: "dmb-usb-step-label" }, t(key)),
6754
+ index < 2 ? h("span", { className: "dmb-usb-step-arrow" }, "→") : null,
6755
+ ),
6756
+ ),
6757
+ ),
6758
+ h(
6759
+ "div",
6760
+ { className: "dmb-usb-field" },
6761
+ h("span", { className: "dmb-usb-field-icon", "aria-hidden": "true" }, h(Icon, { name: "usb", size: 16 })),
6762
+ h("input", {
6763
+ className: "dmb-input dmb-input--mono",
6764
+ type: "text",
6765
+ value: portablePath,
6766
+ placeholder: t("portablePlaceholder"),
6767
+ spellCheck: false,
6768
+ "aria-label": t("portablePlaceholder"),
6769
+ disabled: portableBusy !== null,
6770
+ onChange: (event) => setPortablePath(event.target.value),
6771
+ }),
6772
+ ),
6773
+ h(
6774
+ "div",
6775
+ { className: "dmb-usb-actions" },
6776
+ h(
6777
+ "button",
6778
+ { type: "button", className: "dmb-btn", "data-tone": "primary", disabled: portableBusy !== null, onClick: () => void runPortable("export") },
6779
+ portableBusy === "export" ? h(Spinner) : h(Icon, { name: "save", size: 14 }),
6780
+ t("portableExport"),
6781
+ ),
6782
+ h(
6783
+ "button",
6784
+ { type: "button", className: "dmb-btn", disabled: portableBusy !== null, onClick: () => void runPortable("scan") },
6785
+ portableBusy === "scan" ? h(Spinner) : h(Icon, { name: "scan", size: 14 }),
6786
+ t("portableScan"),
6787
+ ),
6788
+ h(
6789
+ "button",
6790
+ { type: "button", className: "dmb-btn", disabled: portableBusy !== null || portableProjects.length === 0, onClick: () => void runPortable("import") },
6791
+ portableBusy === "import" ? h(Spinner) : h(Icon, { name: "check", size: 14 }),
6792
+ t("portableImport"),
6793
+ ),
6794
+ ),
6795
+ portableMessage !== null ? h(Banner, { tone: portableMessage.tone ?? "ok" }, portableMessage.text) : null,
6796
+ portableProjects.length > 0
6797
+ ? h(
6798
+ "div",
6799
+ { className: "dmb-list dmb-portable-projects" },
6800
+ portableProjects.map((project, index) => {
6801
+ const fileCount = Array.isArray(project?.files) ? project.files.length : Number(project?.files) || 0;
6802
+ return h(
6803
+ "div",
6804
+ { className: "dmb-row", key: String(project?.slug ?? index) },
6805
+ h(
6806
+ "div",
6807
+ { className: "dmb-row-text" },
6808
+ h("div", { className: "dmb-row-title" }, project?.name || project?.slug || "—"),
6809
+ h("div", { className: "dmb-row-desc" }, `${project?.slug || "—"} · ${fileCount}`),
6810
+ ),
6811
+ h(Chip, { tone: portableStatusTone(project?.status) }, t(portableStatusKey(project?.status))),
6812
+ );
6813
+ }),
6814
+ )
6815
+ : null,
6816
+ h("div", { className: "dmb-portable-hint" }, t("portableHint")),
6817
+ h("div", { className: "dmb-usb-cross" }, h(Icon, { name: "repo", size: 13 }), h("span", null, t("portableCross"))),
6818
+ );
6819
+
6820
+ /* ---------------- body ---------------- */
6821
+
6822
+ let body;
6823
+ if (phase === "loading" || (status === null && phase !== "error")) body = loadingCard;
6824
+ else if (phase === "error" && status === null) body = errorCard;
6825
+ else if (status === null) body = loadingCard;
6826
+ else if (configured === true) body = statusCard;
6827
+ else if (github.available !== true) body = ghMissingCard;
6828
+ else if (github.authenticated !== true) body = ghLoginCard;
6829
+ else body = repoSetupCard;
6830
+
6831
+ const showSections = status !== null && phase === "ready";
6832
+
6833
+ return h(
6834
+ "div",
6835
+ { className: "dmb-page", "aria-busy": busy !== null ? "true" : "false" },
6836
+ head,
6837
+ bannersBlock,
6838
+ body,
6839
+ showSections ? howCard : null,
6840
+ showSections ? securityCard : null,
6841
+ showSections ? conflictsCard : null,
6842
+ showSections ? projectsCard : null,
6843
+ showSections ? historyCard : null,
6844
+ showSections ? portableCard : null,
6845
+ );
6846
+ }
6847
+
6848
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
6849
+ name: "settings.section",
6850
+ id: "memory-bridge",
6851
+ order: 35,
6852
+ label: () => t("nav"),
6853
+ locale: NS,
6854
+ inject: () => ({ call: callbacks.call }),
6855
+ }, MemorySyncSection)), "memory-bridge: settings section");
6856
+
6857
+ try {
6858
+ window.__DSH_MEMORY_BRIDGE_UI__ = {
6859
+ call: callbacks.call,
6860
+ openUrl: callbacks.openUrl,
6861
+ };
6862
+ } catch {
6863
+ /* hardened page */
6864
+ }
6865
+
6866
+ console.info("[memory-bridge] ready");
6867
+ }
6868
+
6869
+ exports.apply = apply;
6870
+ exports.inject = inject;
6871
+ return module.exports;
6872
+ })());
6873
+
6874
+ /* ---- dsh-mcp-manager ---- */
6875
+ __parts.push((function () {
6876
+ var module = { exports: {} };
6877
+ var exports = module.exports;
6878
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6879
+ const React = require("react");
6880
+ const h = React.createElement;
6881
+ const useEffect = React.useEffect;
6882
+ const useState = React.useState;
6883
+
6884
+ const RPC_NS = "dsh-mcp-manager";
6885
+
6886
+ const CSS = `
6887
+ .mcp-root{--mcp-line:var(--dsw-alias-border-l1,rgba(20,40,36,.13));--mcp-soft:var(--dsw-alias-bg-layer-1,#f4f7f5);--mcp-panel:var(--dsw-alias-bg-layer-1,#fff);--mcp-text:var(--dsw-alias-label-primary,#17231f);--mcp-muted:var(--dsw-alias-label-secondary,#61706b);--mcp-accent:var(--dsw-alias-brand-primary,#0f766e);--mcp-accent-soft:color-mix(in srgb,var(--mcp-accent) 9%,transparent);--mcp-warn:#a16207;display:flex;flex-direction:column;gap:20px;max-width:1040px;min-width:0;color:var(--mcp-text)}
6888
+ .mcp-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap}
6889
+ .mcp-eyebrow{font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--mcp-accent);margin:0 0 6px}
6890
+ .mcp-title{font-size:clamp(24px,2.6vw,32px);line-height:1.12;letter-spacing:-.025em;margin:0;font-weight:680}
6891
+ .mcp-sub{margin:8px 0 0;max-width:64ch;color:var(--mcp-muted);font-size:14px;line-height:1.5}
6892
+ .mcp-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
6893
+ .mcp-btn{appearance:none;border:1px solid var(--mcp-line);background:var(--mcp-panel);color:var(--mcp-text);border-radius:10px;padding:9px 13px;font:inherit;font-size:13px;font-weight:600;line-height:1;cursor:pointer;display:inline-flex;align-items:center;gap:7px;min-height:38px;transition:border-color .16s ease,background .16s ease,transform .16s ease}
6894
+ .mcp-btn:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
6895
+ .mcp-btn:active{transform:translateY(1px)}
6896
+ .mcp-btn:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}
6897
+ .mcp-btn.primary{background:var(--mcp-accent);border-color:var(--mcp-accent);color:#fff}
6898
+ .mcp-btn.primary:hover{filter:brightness(1.06);background:var(--mcp-accent)}
6899
+ .mcp-btn.danger{color:#9f1239;border-color:rgba(159,18,57,.22)}
6900
+ .mcp-btn.ghost{background:transparent}
6901
+ .mcp-btn[disabled]{opacity:.5;cursor:not-allowed}
6902
+ .mcp-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
6903
+ .mcp-stat{background:linear-gradient(150deg,var(--mcp-panel),var(--mcp-soft));border:1px solid var(--mcp-line);border-radius:14px;padding:14px 16px;min-width:0}
6904
+ .mcp-stat b{display:block;font-size:24px;letter-spacing:-.03em;line-height:1.1;font-variant-numeric:tabular-nums}
6905
+ .mcp-stat span{display:block;margin-top:5px;color:var(--mcp-muted);font-size:11px;font-weight:650;letter-spacing:.08em;text-transform:uppercase}
6906
+ .mcp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
6907
+ .mcp-card{border:1px solid var(--mcp-line);background:var(--mcp-panel);border-radius:16px;padding:16px;min-width:0;display:flex;flex-direction:column;gap:12px;box-shadow:0 1px 0 rgba(20,40,36,.03)}
6908
+ .mcp-card-top{display:flex;align-items:flex-start;gap:10px;justify-content:space-between;min-width:0}
6909
+ .mcp-card-id{display:flex;align-items:center;gap:9px;min-width:0}
6910
+ .mcp-dot{width:8px;height:8px;border-radius:50%;background:#94a3b8;flex:0 0 auto;box-shadow:0 0 0 3px rgba(148,163,184,.16)}
6911
+ .mcp-dot.on{background:#0f9f6e;box-shadow:0 0 0 3px rgba(15,159,110,.14)}
6912
+ .mcp-dot.err{background:#dc2626;box-shadow:0 0 0 3px rgba(220,38,38,.12)}
6913
+ .mcp-name{font-size:15px;font-weight:680;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6914
+ .mcp-badge{font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--mcp-muted);border:1px solid var(--mcp-line);border-radius:999px;padding:3px 7px;white-space:nowrap}
6915
+ .mcp-meta{display:flex;gap:10px;flex-wrap:wrap;color:var(--mcp-muted);font-size:12px;line-height:1.45}
6916
+ .mcp-meta code{font:inherit;color:var(--mcp-text);background:var(--mcp-soft);border-radius:5px;padding:1px 5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6917
+ .mcp-error{color:#9f1239;background:rgba(159,18,57,.06);border:1px solid rgba(159,18,57,.16);border-radius:9px;padding:8px 10px;font-size:12px;line-height:1.4;word-break:break-word}
6918
+ .mcp-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-top:1px solid var(--mcp-line);padding-top:10px;margin-top:auto}
6919
+ .mcp-switch{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--mcp-muted);cursor:pointer;min-height:32px}
6920
+ .mcp-switch input{position:absolute;opacity:0;width:1px;height:1px}
6921
+ .mcp-switch i{width:30px;height:18px;border-radius:999px;background:#cbd5e1;position:relative;transition:background .16s ease;flex:0 0 auto}
6922
+ .mcp-switch i:after{content:"";position:absolute;width:14px;height:14px;left:2px;top:2px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform .16s ease}
6923
+ .mcp-switch input:checked+i{background:var(--mcp-accent)}
6924
+ .mcp-switch input:checked+i:after{transform:translateX(12px)}
6925
+ .mcp-switch input:focus-visible+i{outline:2px solid var(--mcp-accent);outline-offset:2px}
6926
+ .mcp-actions{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap}
6927
+ .mcp-actions .mcp-btn{padding:7px 9px;min-height:32px;font-size:12px}
6928
+ .mcp-tools{border-top:1px dashed var(--mcp-line);padding-top:10px;display:flex;flex-direction:column;gap:6px}
6929
+ .mcp-tool{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.45;min-width:0}
6930
+ .mcp-tool code{color:var(--mcp-accent);font:inherit;font-weight:650;white-space:nowrap}
6931
+ .mcp-tool span{color:var(--mcp-muted);overflow:hidden;text-overflow:ellipsis}
6932
+ .mcp-empty{border:1px dashed var(--mcp-line);border-radius:18px;padding:26px;background:linear-gradient(160deg,var(--mcp-soft),transparent 75%);display:flex;flex-direction:column;gap:12px;align-items:flex-start}
6933
+ .mcp-empty h3{margin:0;font-size:18px;letter-spacing:-.015em}
6934
+ .mcp-empty p{margin:0;color:var(--mcp-muted);font-size:13px;line-height:1.5;max-width:60ch}
6935
+ .mcp-presets{display:flex;gap:8px;flex-wrap:wrap}
6936
+ .mcp-preset{border:1px solid var(--mcp-line);background:var(--mcp-panel);border-radius:12px;padding:10px 12px;cursor:pointer;text-align:left;min-width:150px;font:inherit;color:inherit}
6937
+ .mcp-preset:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
6938
+ .mcp-preset b{display:block;font-size:13px}
6939
+ .mcp-preset small{display:block;color:var(--mcp-muted);font-size:11px;line-height:1.35;margin-top:3px}
6940
+ .mcp-note{color:var(--mcp-muted);font-size:11px;line-height:1.45;margin:0}
6941
+ .mcp-loading{color:var(--mcp-muted);font-size:13px;padding:18px 0}
6942
+ .mcp-overlay{position:fixed;inset:0;background:rgba(10,20,18,.42);backdrop-filter:blur(4px);z-index:80;display:flex;align-items:center;justify-content:center;padding:20px}
6943
+ .mcp-modal{width:min(640px,100%);max-height:min(86vh,820px);overflow:auto;background:var(--mcp-panel);border:1px solid var(--mcp-line);border-radius:20px;box-shadow:0 24px 80px rgba(10,20,18,.24);padding:22px;display:flex;flex-direction:column;gap:16px}
6944
+ .mcp-modal-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}
6945
+ .mcp-modal h2{margin:0;font-size:20px;letter-spacing:-.02em}
6946
+ .mcp-modal p{margin:4px 0 0;color:var(--mcp-muted);font-size:13px;line-height:1.45}
6947
+ .mcp-form{display:grid;grid-template-columns:1fr 1fr;gap:12px}
6948
+ .mcp-field{display:flex;flex-direction:column;gap:5px;min-width:0}
6949
+ .mcp-field.wide{grid-column:1/-1}
6950
+ .mcp-field label{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--mcp-muted)}
6951
+ .mcp-field input,.mcp-field select,.mcp-field textarea{width:100%;box-sizing:border-box;border:1px solid var(--mcp-line);border-radius:9px;background:var(--mcp-panel);color:var(--mcp-text);font:inherit;font-size:13px;padding:9px 10px;min-height:38px}
6952
+ .mcp-field textarea{min-height:74px;resize:vertical;line-height:1.45}
6953
+ .mcp-field input:focus,.mcp-field select:focus,.mcp-field textarea:focus{outline:2px solid var(--mcp-accent);outline-offset:1px;border-color:transparent}
6954
+ .mcp-check{display:flex;gap:8px;align-items:flex-start;font-size:12px;color:var(--mcp-muted);line-height:1.4}
6955
+ .mcp-check input{margin-top:2px}
6956
+ .mcp-form-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;border-top:1px solid var(--mcp-line);padding-top:14px}
6957
+ .mcp-toast{font-size:12px;border-radius:9px;padding:8px 10px;background:var(--mcp-accent-soft);color:var(--mcp-accent);border:1px solid rgba(15,118,110,.18)}
6958
+ @media (max-width:860px){.mcp-grid{grid-template-columns:1fr}.mcp-stats{grid-template-columns:1fr 1fr}.mcp-form{grid-template-columns:1fr}.mcp-field.wide{grid-column:auto}}
6959
+ @media (max-width:520px){.mcp-stats{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.mcp-stat{padding:10px 12px}.mcp-stat b{font-size:20px}.mcp-stat span{font-size:9px;letter-spacing:.04em}.mcp-head{align-items:flex-start}.mcp-actions{margin-left:0;width:100%}.mcp-root{gap:16px}.mcp-modal{padding:18px;border-radius:16px}.mcp-overlay{padding:10px}}
6960
+ @media (prefers-reduced-motion:reduce){.mcp-btn,.mcp-switch i,.mcp-switch i:after{transition:none}}
6961
+ `;
6962
+
6963
+ const PRESETS = [
6964
+ { id: "github", title: "GitHub", subtitle: "Issues, PR и код", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-github", tokenEnv: "GITHUB_TOKEN", tokenLabel: "GitHub token", trusted: true },
6965
+ { id: "postgres", title: "PostgreSQL", subtitle: "Схема и read-only запросы", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-postgres\npostgresql://readonly@localhost:5432/postgres", tokenEnv: "PGPASSWORD", tokenLabel: "Пароль read-only роли", trusted: true },
6966
+ { id: "filesystem", title: "Filesystem", subtitle: "Папка проекта, без токена", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-filesystem\n.", trusted: true },
6967
+ ];
6968
+
6969
+ function call(ctx, operation, payload, timeout = 60000) {
6970
+ let signal;
6971
+ try { signal = AbortSignal.timeout(timeout); } catch { signal = undefined; }
6972
+ return ctx.connection.rpc.call("/api", `${RPC_NS}/${operation}`, payload ?? {}, signal).then((result) => {
6973
+ if (!result || result.ok !== true) throw new Error(result?.error?.message || "Ошибка RPC");
6974
+ return result.value;
6975
+ });
6976
+ }
6977
+
6978
+ function Toggle({ checked, onChange, label, disabled }) {
6979
+ return h("label", { className: "mcp-switch" },
6980
+ h("input", { type: "checkbox", checked, disabled, onChange: (event) => onChange(event.target.checked) }),
6981
+ h("i", { "aria-hidden": "true" }),
6982
+ label,
6983
+ );
6984
+ }
6985
+
6986
+ function ServerCard({ server, busy, onToggle, onTest, onTools, onRemove, tools }) {
6987
+ const [open, setOpen] = useState(false);
6988
+ const connected = server.connected;
6989
+ const status = connected ? "on" : (server.error ? "err" : "off");
6990
+ return h("article", { className: "mcp-card" },
6991
+ h("div", { className: "mcp-card-top" },
6992
+ h("div", { className: "mcp-card-id" },
6993
+ h("span", { className: `mcp-dot ${status}`, "aria-hidden": "true" }),
6994
+ h("span", { className: "mcp-name", title: server.id }, server.id),
6995
+ h("span", { className: "mcp-badge" }, server.scope === "project" ? "проект" : "глобально"),
6996
+ ),
6997
+ h("span", { className: "mcp-badge" }, server.transport === "http" ? "HTTP" : "stdio"),
6998
+ ),
6999
+ h("div", { className: "mcp-meta" },
7000
+ h("span", null, connected ? `подключён · ${server.tools} tools` : server.error ? "ошибка" : "не запущен"),
7001
+ server.url ? h("code", { title: server.url }, server.url) : h("code", { title: server.command }, server.command || "—"),
7002
+ ),
7003
+ server.error ? h("div", { className: "mcp-error" }, server.error) : null,
7004
+ h("div", { className: "mcp-controls" },
7005
+ h(Toggle, { checked: server.enabled, label: "Включён", disabled: busy, onChange: (value) => onToggle(server.id, { enabled: value }) }),
7006
+ h(Toggle, { checked: server.trusted, label: "Доверен", disabled: busy, onChange: (value) => onToggle(server.id, { trusted: value }) }),
7007
+ h("div", { className: "mcp-actions" },
7008
+ h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onTest(server.id) }, "Проверить"),
7009
+ h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => { setOpen(!open); if (!open) onTools(server.id); } }, open ? "Скрыть" : "Tools"),
7010
+ h("button", { className: "mcp-btn danger ghost", disabled: busy, onClick: () => onRemove(server.id) }, "Удалить"),
7011
+ ),
7012
+ ),
7013
+ open ? h("div", { className: "mcp-tools" },
7014
+ tools === undefined ? h("div", { className: "mcp-meta" }, "Загрузка…")
7015
+ : tools.length === 0 ? h("div", { className: "mcp-meta" }, "Нет tools.")
7016
+ : tools.map((tool) => h("div", { className: "mcp-tool", key: tool.name },
7017
+ h("code", null, tool.name),
7018
+ h("span", { title: tool.description }, tool.description || "без описания"),
7019
+ )),
7020
+ ) : null,
7021
+ );
7022
+ }
7023
+
7024
+ function ServerForm({ initial, busy, onCancel, onSave }) {
7025
+ const [form, setForm] = useState(initial);
7026
+ const set = (patch) => setForm((current) => ({ ...current, ...patch }));
7027
+ const submit = (event) => {
7028
+ event.preventDefault();
7029
+ onSave({
7030
+ ...form,
7031
+ args: String(form.args ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
7032
+ });
7033
+ };
7034
+ return h("div", { className: "mcp-overlay", onMouseDown: (event) => { if (event.target === event.currentTarget) onCancel(); } },
7035
+ h("form", { className: "mcp-modal", role: "dialog", "aria-modal": "true", "aria-label": "Добавить MCP-сервер", onSubmit: submit },
7036
+ h("div", { className: "mcp-modal-head" },
7037
+ h("div", null,
7038
+ h("h2", null, initial.edit ? "Настроить сервер" : "Новый MCP-сервер"),
7039
+ h("p", null, "Один раз добавьте подключение — дальше агент сам увидит его инструменты."),
7040
+ ),
7041
+ h("button", { type: "button", className: "mcp-btn ghost", onClick: onCancel, "aria-label": "Закрыть" }, "Закрыть"),
7042
+ ),
7043
+ h("div", { className: "mcp-form" },
7044
+ h("div", { className: "mcp-field" },
7045
+ h("label", { htmlFor: "mcp-id" }, "ID"),
7046
+ h("input", { id: "mcp-id", value: form.id, disabled: initial.edit, placeholder: "github", onChange: (event) => set({ id: event.target.value.toLowerCase().replace(/[^a-z0-9_-]/gu, "-") }) }),
7047
+ ),
7048
+ h("div", { className: "mcp-field" },
7049
+ h("label", { htmlFor: "mcp-transport" }, "Транспорт"),
7050
+ h("select", { id: "mcp-transport", value: form.transport, onChange: (event) => set({ transport: event.target.value }) },
7051
+ h("option", { value: "stdio" }, "stdio — локальная команда"),
7052
+ h("option", { value: "http" }, "HTTP — удалённый сервер"),
7053
+ ),
7054
+ ),
7055
+ form.transport === "stdio" ? h(React.Fragment, null,
7056
+ h("div", { className: "mcp-field wide" },
7057
+ h("label", { htmlFor: "mcp-command" }, "Команда"),
7058
+ h("input", { id: "mcp-command", value: form.command, placeholder: "npx", onChange: (event) => set({ command: event.target.value }) }),
7059
+ ),
7060
+ h("div", { className: "mcp-field wide" },
7061
+ h("label", { htmlFor: "mcp-args" }, "Аргументы — по одному в строке"),
7062
+ h("textarea", { id: "mcp-args", value: form.args, placeholder: "-y\n@modelcontextprotocol/server-github", onChange: (event) => set({ args: event.target.value }) }),
7063
+ ),
7064
+ h("div", { className: "mcp-field" },
7065
+ h("label", { htmlFor: "mcp-token-env" }, "Переменная токена"),
7066
+ h("input", { id: "mcp-token-env", value: form.tokenEnv, placeholder: "GITHUB_TOKEN", onChange: (event) => set({ tokenEnv: event.target.value }) }),
7067
+ ),
7068
+ ) : h(React.Fragment, null,
7069
+ h("div", { className: "mcp-field wide" },
7070
+ h("label", { htmlFor: "mcp-url" }, "URL"),
7071
+ h("input", { id: "mcp-url", value: form.url, placeholder: "https://example.com/mcp", onChange: (event) => set({ url: event.target.value }) }),
7072
+ ),
7073
+ h("div", { className: "mcp-field" },
7074
+ h("label", { htmlFor: "mcp-token-header" }, "Заголовок токена"),
7075
+ h("input", { id: "mcp-token-header", value: form.tokenHeader, placeholder: "Authorization", onChange: (event) => set({ tokenHeader: event.target.value }) }),
7076
+ ),
7077
+ ),
7078
+ h("div", { className: "mcp-field wide" },
7079
+ h("label", { htmlFor: "mcp-token" }, form.tokenLabel || "Токен / пароль"),
7080
+ h("input", { id: "mcp-token", type: "password", value: form.token, placeholder: form.tokenPlaceholder || "оставьте пустым, если не нужен", autoComplete: "off", onChange: (event) => set({ token: event.target.value }) }),
7081
+ ),
7082
+ h("label", { className: "mcp-check wide" },
7083
+ h("input", { type: "checkbox", checked: form.trusted, onChange: (event) => set({ trusted: event.target.checked }) }),
7084
+ h("span", null, "Я доверяю этому серверу. Он сможет выполнять команды и читать данные, к которым у него есть доступ."),
7085
+ ),
7086
+ ),
7087
+ h("p", { className: "mcp-note" }, "Секрет сохраняется в ~/.dsh/mcp.secrets.json с правами 0600 и не попадает в mcp.json или лог."),
7088
+ h("div", { className: "mcp-form-actions" },
7089
+ h("button", { type: "button", className: "mcp-btn", onClick: onCancel }, "Отмена"),
7090
+ h("button", { type: "submit", className: "mcp-btn primary", disabled: busy }, busy ? "Сохраняем…" : "Сохранить и подключить"),
7091
+ ),
7092
+ ),
7093
+ );
7094
+ }
7095
+
7096
+ function McpSection() {
7097
+ const [data, setData] = useState(null);
7098
+ const [error, setError] = useState(null);
7099
+ const [busy, setBusy] = useState(false);
7100
+ const [form, setForm] = useState(null);
7101
+ const [tools, setTools] = useState({});
7102
+ const [toast, setToast] = useState("");
7103
+
7104
+ const load = async () => {
7105
+ try {
7106
+ const value = await call(window.__DSH_MCP_CTX__, "status");
7107
+ setData(value);
7108
+ setError(null);
7109
+ } catch (cause) {
7110
+ setError(cause instanceof Error ? cause.message : String(cause));
7111
+ }
7112
+ };
7113
+
7114
+ useEffect(() => { void load(); }, []);
7115
+
7116
+ const run = async (operation, payload, message) => {
7117
+ setBusy(true);
7118
+ setError(null);
7119
+ try {
7120
+ await call(window.__DSH_MCP_CTX__, operation, payload);
7121
+ await load();
7122
+ if (message) { setToast(message); setTimeout(() => setToast(""), 3000); }
7123
+ } catch (cause) {
7124
+ setError(cause instanceof Error ? cause.message : String(cause));
7125
+ } finally {
7126
+ setBusy(false);
7127
+ }
7128
+ };
7129
+
7130
+ const presets = PRESETS;
7131
+ const openPreset = (preset) => setForm({
7132
+ edit: false,
7133
+ id: preset.id,
7134
+ transport: preset.transport,
7135
+ command: preset.command || "",
7136
+ args: preset.args || "",
7137
+ url: "",
7138
+ tokenEnv: preset.tokenEnv || "",
7139
+ tokenHeader: "Authorization",
7140
+ tokenLabel: preset.tokenLabel || "Токен",
7141
+ tokenPlaceholder: preset.tokenPlaceholder || "",
7142
+ token: "",
7143
+ trusted: preset.trusted === true,
7144
+ });
7145
+ const openManual = () => setForm({ edit: false, id: "", transport: "stdio", command: "npx", args: "-y\n", url: "", tokenEnv: "MCP_TOKEN", tokenHeader: "Authorization", tokenLabel: "Токен", tokenPlaceholder: "", token: "", trusted: false });
7146
+
7147
+ const servers = data?.servers ?? [];
7148
+ const connected = servers.filter((server) => server.connected).length;
7149
+ const totalTools = servers.reduce((sum, server) => sum + (Number(server.tools) || 0), 0);
7150
+
7151
+ return h("section", { className: "mcp-root", "aria-label": "MCP-подключения" },
7152
+ h("div", { className: "mcp-head" },
7153
+ h("div", null,
7154
+ h("p", { className: "mcp-eyebrow" }, "MCP · Model Context Protocol"),
7155
+ h("h2", { className: "mcp-title" }, "Подключения"),
7156
+ h("p", { className: "mcp-sub" }, "Внешние системы для агента: GitHub, PostgreSQL, файлы и любые MCP-серверы. Добавьте один раз — инструменты появятся в новом чате."),
7157
+ ),
7158
+ h("div", { className: "mcp-toolbar" },
7159
+ h("button", { className: "mcp-btn primary", onClick: openManual, disabled: busy }, "Добавить сервер"),
7160
+ ),
7161
+ ),
7162
+ error ? h("div", { className: "mcp-error", role: "alert" }, error) : null,
7163
+ toast ? h("div", { className: "mcp-toast", role: "status" }, toast) : null,
7164
+ h("div", { className: "mcp-stats" },
7165
+ h("div", { className: "mcp-stat" }, h("b", null, servers.length), h("span", null, "серверов")),
7166
+ h("div", { className: "mcp-stat" }, h("b", null, connected), h("span", null, "подключено")),
7167
+ h("div", { className: "mcp-stat" }, h("b", null, totalTools), h("span", null, "инструментов")),
7168
+ ),
7169
+ data === null ? h("div", { className: "mcp-loading" }, "Загружаем подключения…") : null,
7170
+ data !== null && servers.length === 0 ? h("div", { className: "mcp-empty" },
7171
+ h("h3", null, "Пока пусто — но это быстро"),
7172
+ h("p", null, "Начните с готового пресета. Понадобится только токен, если он нужен серверу; для Filesystem — ничего."),
7173
+ h("div", { className: "mcp-presets" }, presets.map((preset) => h("button", { key: preset.id, className: "mcp-preset", onClick: () => openPreset(preset) },
7174
+ h("b", null, preset.title), h("small", null, preset.subtitle),
7175
+ ))),
7176
+ h("button", { className: "mcp-btn ghost", onClick: openManual }, "Настроить вручную"),
7177
+ ) : null,
7178
+ servers.length > 0 ? h("div", { className: "mcp-grid" }, servers.map((server) => h(ServerCard, {
7179
+ key: server.id,
7180
+ server,
7181
+ busy,
7182
+ tools: tools[server.id],
7183
+ onToggle: (id, flags) => run("toggle", { id, ...flags }, "Настройки сервера обновлены"),
7184
+ onTest: async (id) => {
7185
+ setBusy(true);
7186
+ try {
7187
+ const result = await call(window.__DSH_MCP_CTX__, "test", { id });
7188
+ setToast(result.connected ? `Подключён · ${result.tools} tools` : `Ошибка: ${result.error}`);
7189
+ setTimeout(() => setToast(""), 4000);
7190
+ await load();
7191
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
7192
+ finally { setBusy(false); }
7193
+ },
7194
+ onTools: async (id) => {
7195
+ try {
7196
+ const result = await call(window.__DSH_MCP_CTX__, "tools", { id });
7197
+ setTools((current) => ({ ...current, [id]: result.tools }));
7198
+ if (result.error) setError(result.error);
7199
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
7200
+ },
7201
+ onRemove: (id) => {
7202
+ if (window.confirm(`Удалить MCP-сервер «${id}»?`)) void run("remove", { id }, "Сервер удалён");
7203
+ },
7204
+ }))) : null,
7205
+ servers.length > 0 ? h("p", { className: "mcp-note" }, `Секреты: ${data?.secretsFile || "~/.dsh/mcp.secrets.json"} · новые tools появятся в новом чате после подключения.`) : null,
7206
+ form ? h(ServerForm, {
7207
+ initial: form,
7208
+ busy,
7209
+ onCancel: () => setForm(null),
7210
+ onSave: async (payload) => {
7211
+ await run("save", payload, "Сервер сохранён и подключён");
7212
+ setForm(null);
7213
+ },
7214
+ }) : null,
7215
+ );
7216
+ }
7217
+
7218
+ function apply(ctx) {
7219
+ window.__DSH_MCP_CTX__ = ctx;
7220
+ ctx.effect(() => {
7221
+ const style = document.createElement("style");
7222
+ style.id = "dsh-mcp-manager";
7223
+ style.textContent = CSS;
7224
+ document.head.appendChild(style);
7225
+ return () => style.remove();
7226
+ }, "mcp-manager: styles");
7227
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
7228
+ name: "settings.section",
7229
+ id: "mcp-manager",
7230
+ order: 35,
7231
+ label: () => "MCP", }, McpSection)), "mcp-manager: settings");
7232
+ }
7233
+
7234
+ exports.apply = apply;
7235
+ exports.inject = ["slots", "connection"];
7236
+ return module.exports;
7237
+ })());
7238
+
7239
+ const __partNames = ["dsh-gkb-ru-ui","dsh-token-lab","dsh-peak-guard","dsh-session-move","dsh-profile-transfer","dsh-memory-bridge","dsh-mcp-manager"];
4151
7240
  const __kitErrors = [];
4152
7241
  const __pushKitError = (label, error) => {
4153
7242
  const message = String(error && error.message ? error.message : error);
@@ -4155,7 +7244,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
4155
7244
  if (__kitErrors.length <= 5) console.error("[dsh-desktop-kit] registration failed:", label, message);
4156
7245
  };
4157
7246
  try {
4158
- window.__DSH_DESKTOP_KIT__ = { version: "2.2.1", parts: __partNames, errors: __kitErrors, ready: false };
7247
+ window.__DSH_DESKTOP_KIT__ = { version: "2.7.2", parts: __partNames, errors: __kitErrors, ready: false };
4159
7248
  } catch {}
4160
7249
  const __inject = [];
4161
7250
  for (const __part of __parts) {