dsh-desktop-kit 1.0.0 → 2.0.1

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 (144) hide show
  1. package/README.md +106 -41
  2. package/client.js +780 -28
  3. package/docs/ARCHITECTURE.md +114 -0
  4. package/docs/COMPATIBILITY.md +61 -0
  5. package/docs/INSTALL.md +97 -0
  6. package/docs/PRIVACY.md +50 -0
  7. package/docs/ROADMAP.md +57 -0
  8. package/docs/TOKENS.md +118 -0
  9. package/docs/TROUBLESHOOTING.md +68 -0
  10. package/docs/images/hero.png +0 -0
  11. package/index.js +4 -0
  12. package/package.json +15 -5
  13. package/plugins/dsh-gkb-ru-ui/lib/client.js +276 -28
  14. package/plugins/dsh-gkb-ru-ui/lib/index.js +45 -1
  15. package/plugins/dsh-gkb-ru-ui/lib/skills.js +195 -1
  16. package/plugins/dsh-gkb-ru-ui/package.json +2 -2
  17. package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
  18. package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
  19. package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
  20. package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
  21. package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
  22. package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
  23. package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
  24. package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
  25. package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
  26. package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
  27. package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
  28. package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
  29. package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
  30. package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
  31. package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
  32. package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
  33. package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
  34. package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
  35. package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
  36. package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
  37. package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
  38. package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
  39. package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
  40. package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
  41. package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
  42. package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
  43. package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
  44. package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
  45. package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
  46. package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
  47. package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
  48. package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
  49. package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
  50. package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
  51. package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
  52. package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
  53. package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
  54. package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
  55. package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
  56. package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
  57. package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
  58. package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
  59. package/plugins/dsh-skill-autopilot/index.js +444 -0
  60. package/plugins/dsh-skill-autopilot/package.json +19 -0
  61. package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
  62. package/plugins/dsh-token-lab/lib/client.js +504 -0
  63. package/plugins/dsh-token-lab/lib/index.js +275 -0
  64. package/plugins/dsh-token-lab/package.json +26 -0
  65. package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
  66. package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
  67. package/skills/code-review-checklist/SKILL.md +48 -30
  68. package/skills/content-writing/SKILL.md +64 -327
  69. package/skills/content-writing/references/process.md +123 -0
  70. package/skills/content-writing/references/quick-recipes.md +99 -0
  71. package/skills/content-writing/references/rules.md +85 -0
  72. package/skills/content-writing/references/sources.md +49 -0
  73. package/skills/context-economy/SKILL.md +60 -0
  74. package/skills/context-economy/references/long-session.md +35 -0
  75. package/skills/context-economy/references/token-math.md +42 -0
  76. package/skills/context-economy/references/tool-recipes.md +34 -0
  77. package/skills/database-safety/SKILL.md +40 -24
  78. package/skills/debugging-playbook/SKILL.md +30 -27
  79. package/skills/design-critique/SKILL.md +57 -110
  80. package/skills/design-critique/references/a11y.md +25 -0
  81. package/skills/design-critique/references/diagnoses.md +33 -0
  82. package/skills/design-critique/references/quick-tests.md +30 -0
  83. package/skills/design-critique/references/report-template.md +63 -0
  84. package/skills/design-critique/references/rubric-fast.md +73 -0
  85. package/skills/dev-workflow/SKILL.md +41 -34
  86. package/skills/dev-workflow/references/definition-of-done.md +44 -0
  87. package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
  88. package/skills/dev-workflow/references/stages.md +67 -0
  89. package/skills/git-and-ci/SKILL.md +46 -26
  90. package/skills/pg-upgrade-verify/SKILL.md +54 -35
  91. package/skills/project-deep-dive/SKILL.md +39 -164
  92. package/skills/project-deep-dive/references/recon-commands.md +12 -0
  93. package/skills/project-deep-dive/references/section-templates.md +2 -0
  94. package/skills/project-deep-dive/references/tool-and-process.md +86 -0
  95. package/skills/project-memory-maintenance/SKILL.md +39 -154
  96. package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
  97. package/skills/project-memory-maintenance/references/journal-template.md +2 -0
  98. package/skills/project-memory-maintenance/references/section-rules.md +2 -0
  99. package/skills/release-and-deploy/SKILL.md +46 -32
  100. package/skills/requirements-and-planning/SKILL.md +44 -110
  101. package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
  102. package/skills/requirements-and-planning/references/process-steps.md +52 -0
  103. package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
  104. package/skills/security-review/SKILL.md +33 -29
  105. package/skills/seo/SKILL.md +53 -305
  106. package/skills/seo/references/antipatterns.md +66 -0
  107. package/skills/seo/references/audit-checklist.md +14 -0
  108. package/skills/seo/references/content-seo.md +180 -174
  109. package/skills/seo/references/eeat-medical.md +2 -0
  110. package/skills/seo/references/measurement.md +28 -1
  111. package/skills/seo/references/on-page.md +5 -0
  112. package/skills/seo/references/page-templates.md +56 -0
  113. package/skills/seo/references/stack-recipes.md +2 -0
  114. package/skills/seo/references/structured-data.md +2 -0
  115. package/skills/seo/references/technical-seo.md +2 -0
  116. package/skills/service-desk-conventions/SKILL.md +40 -28
  117. package/skills/service-desk-db-safety/SKILL.md +39 -28
  118. package/skills/service-desk-release/SKILL.md +40 -22
  119. package/skills/service-desk-testing/SKILL.md +47 -33
  120. package/skills/service-desk-ui/SKILL.md +43 -24
  121. package/skills/skills-router/SKILL.md +44 -82
  122. package/skills/skills-router/references/applying-skills.md +25 -0
  123. package/skills/skills-router/references/routing-matrix.md +52 -0
  124. package/skills/testing-strategy/SKILL.md +47 -32
  125. package/skills/ui-design-system/SKILL.md +43 -38
  126. package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
  127. package/skills/ui-design-system/references/tokens-and-components.md +67 -0
  128. package/skills/ui-design-system/references/visual-qa.md +44 -0
  129. package/skills/web-research/SKILL.md +40 -33
  130. package/skills/web-research/references/answer-and-citations.md +60 -0
  131. package/skills/web-research/references/query-recipes.md +44 -0
  132. package/skills/wow-design/SKILL.md +82 -161
  133. package/skills/wow-design/references/anti-patterns.md +3 -0
  134. package/skills/wow-design/references/critique-rubric.md +3 -0
  135. package/skills/wow-design/references/design-brief-template.md +2 -0
  136. package/skills/wow-design/references/inspiration.md +3 -0
  137. package/skills/wow-design/references/presets.md +3 -0
  138. package/skills/wow-design/references/principles.md +3 -0
  139. package/skills/wow-design/references/quick-presets.md +50 -0
  140. package/skills/wow-design/references/report-template.md +21 -0
  141. package/skills/wow-design/references/stack-recipes.md +3 -0
  142. package/skills/wow-design/references/visual-qa.md +3 -0
  143. package/skills/wow-design/references/wow-levers.md +43 -0
  144. package/tools/skill-cost.mjs +76 -0
@@ -70,6 +70,11 @@ window.__ModuleLoader__.load({
70
70
  if (value % 10 >= 2 && value % 10 <= 4 && (value % 100 < 12 || value % 100 > 14)) return value + " запуска";
71
71
  return value + " запусков";
72
72
  }
73
+ function compactTokens(value) {
74
+ const n = Number(value) || 0;
75
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "").replace(".", ",") + "k";
76
+ return String(n);
77
+ }
73
78
  function shortDate(iso) {
74
79
  if (!iso) return "—";
75
80
  try {
@@ -430,7 +435,7 @@ window.__ModuleLoader__.load({
430
435
  { id: "design", label: "Дизайн", color: "#0E6E5E", skills: ["wow-design", "design-critique", "ui-design-system"] },
431
436
  { id: "seo", label: "SEO", color: "#2C5F8A", skills: ["seo"] },
432
437
  { id: "content", label: "Контент", color: "#B36A43", skills: ["content-writing"] },
433
- { id: "process", label: "Процесс", color: "#6B7275", skills: ["dev-workflow", "requirements-and-planning", "skills-router"] },
438
+ { id: "process", label: "Процесс", color: "#6B7275", skills: ["dev-workflow", "requirements-and-planning", "skills-router", "context-economy"] },
434
439
  { id: "quality", label: "Качество", color: "#1F7A57", skills: ["testing-strategy", "debugging-playbook", "code-review-checklist"] },
435
440
  { id: "safety", label: "Данные", color: "#94661C", skills: ["database-safety", "security-review", "pg-upgrade-verify"] },
436
441
  { id: "release", label: "Релиз", color: "#2C5F8A", skills: ["release-and-deploy", "git-and-ci"] },
@@ -438,7 +443,7 @@ window.__ModuleLoader__.load({
438
443
  { id: "memory", label: "Память", color: "#5B4B8A", skills: ["project-deep-dive", "project-memory-maintenance"] }
439
444
  ];
440
445
  const SKILL_NAMES = SKILL_GROUPS.reduce((acc, group) => acc.concat(group.skills), []);
441
- const SKILLS_TILE_STYLE = Object.assign({}, TILE_BASE, { display: "block", padding: "10px 12px", cursor: "pointer" });
446
+ const SKILLS_TILE_STYLE = Object.assign({}, TILE_BASE, { display: "block", padding: "8px 10px", cursor: "pointer" });
442
447
  const SKILLS_HEAD_STYLE = { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "8px" };
443
448
  const SKILLS_COUNT_STYLE = {
444
449
  display: "inline-flex", alignItems: "center", justifyContent: "center",
@@ -451,14 +456,14 @@ window.__ModuleLoader__.load({
451
456
  fontSize: "10px", fontWeight: 500, marginTop: "2px", letterSpacing: "0.02em",
452
457
  color: "var(--dsw-alias-label-secondary, #777)"
453
458
  };
454
- const SKILLS_CHIPS_STYLE = { display: "flex", flexWrap: "wrap", gap: "4px", marginTop: "8px" };
459
+ const SKILLS_CHIPS_STYLE = { display: "flex", flexWrap: "nowrap", gap: "4px", marginTop: "6px", overflow: "hidden", maskImage: "linear-gradient(to right, #000 88%, transparent 100%)", WebkitMaskImage: "linear-gradient(to right, #000 88%, transparent 100%)" };
455
460
  const SKILLS_CHIP_STYLE = {
456
- fontSize: "10px", fontWeight: 600, lineHeight: 1.5,
461
+ fontSize: "9.5px", fontWeight: 600, lineHeight: 1.45,
457
462
  color: "var(--dsw-alias-label-secondary, #666)",
458
463
  border: "1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.10))",
459
464
  borderRadius: "999px", padding: "1px 6px",
460
465
  background: "var(--dsw-alias-settings-card-fill, rgba(0,0,0,.03))",
461
- whiteSpace: "nowrap"
466
+ whiteSpace: "nowrap", flex: "0 0 auto"
462
467
  };
463
468
  const SKILLS_TOGGLE_STYLE = {
464
469
  marginTop: "8px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px",
@@ -522,10 +527,20 @@ window.__ModuleLoader__.load({
522
527
  .wtg9ga_root .GAkraW_groupSection.gkb-collapsed > .wnFoaW_projectRow::after { content: "▸"; }
523
528
  .wtg9ga_root .GAkraW_groupSection.gkb-collapsed > span:not(:first-child) { display: none !important; }
524
529
  .wtg9ga_root .wnFoaW_projectRow:hover::after { opacity: 1; color: var(--dsw-alias-label-primary, #111); }
530
+ /* Short windows: the sidebar must still show the account/settings foot. */
531
+ @media (max-height: 820px) {
532
+ .wtg9ga_root > .wtg9ga_regionArea { min-height: 140px; }
533
+ .gkb-skills-chips { display: none !important; }
534
+ .gkb-skills-active { margin-top: 4px !important; }
535
+ }
536
+ /* Mobile rail is 55px: tiles cannot fit, so they live behind the FAB. */
537
+ @media (max-width: 760px) {
538
+ [data-gkb-tiles] { display: none !important; }
539
+ }
525
540
  `;
526
541
  const SKILLS_CATALOG_CSS = `
527
542
  .gkb-sk-backdrop{position:fixed;inset:0;z-index:9000;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb,#070b0a 46%,transparent);backdrop-filter:blur(4px);animation:gkb-sk-fade .18s ease-out}
528
- .gkb-sk-panel{position:relative;display:flex;flex-direction:column;width:min(1120px,94vw);max-height:88vh;border-radius:18px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);box-shadow:0 48px 140px -50px rgba(0,10,8,.55),0 1px 0 color-mix(in srgb,#fff 65%,transparent) inset;overflow:hidden;animation:gkb-sk-in .28s cubic-bezier(.16,1,.3,1)}
543
+ .gkb-sk-panel{position:relative;display:flex;flex-direction:column;width:min(1120px,94vw);max-height:92vh;border-radius:18px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);box-shadow:0 48px 140px -50px rgba(0,10,8,.55),0 1px 0 color-mix(in srgb,#fff 65%,transparent) inset;overflow:hidden;animation:gkb-sk-in .28s cubic-bezier(.16,1,.3,1)}
529
544
  .gkb-sk-head{flex:none;display:flex;flex-direction:column;gap:12px;padding:22px 24px 14px;background:
530
545
  radial-gradient(120% 160% at 100% 0%,color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 10%,transparent) 0%,transparent 55%),
531
546
  linear-gradient(180deg,color-mix(in srgb,var(--dsw-alias-bg-layer-1,#f6f7f6) 70%,transparent) 0%,transparent 100%);
@@ -538,7 +553,8 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
538
553
  .gkb-sk-sub b{color:var(--dsw-alias-label-primary,#111);font-weight:650}
539
554
  .gkb-sk-close{flex:none;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:transparent;color:var(--dsw-alias-label-secondary,#555);border-radius:9px;width:32px;height:32px;font-size:17px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
540
555
  .gkb-sk-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary,#111)}
541
- .gkb-sk-close:focus-visible,.gkb-sk-card:focus-visible,.gkb-sk-chip:focus-visible,.gkb-sk-btn:focus-visible,.gkb-sk-search:focus-visible,.gkb-sk-sort:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#0E6E5E);outline-offset:2px}
556
+ .gkb-sk-close:focus-visible,.gkb-sk-card:focus-visible,.gkb-sk-chip:focus-visible,.gkb-sk-btn:focus-visible,.gkb-sk-sort:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#0E6E5E);outline-offset:2px}
557
+ .gkb-sk-search:focus,.gkb-sk-search:focus-visible,.gkb-sk-input:focus,.gkb-sk-input:focus-visible{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 55%,transparent);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 13%,transparent)}
542
558
  .gkb-sk-search-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
543
559
  .gkb-sk-search{flex:1 1 280px;min-width:0;box-sizing:border-box;padding:10px 13px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:var(--dsw-alias-bg-layer-1,#f7f8f7);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;line-height:18px}
544
560
  .gkb-sk-search::placeholder{color:var(--dsw-alias-label-tertiary,#98a0a8)}
@@ -594,18 +610,52 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
594
610
  .gkb-sk-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
595
611
  .gkb-sk-btn[data-tone="primary"]{border-color:transparent;background:var(--dsw-alias-label-primary,#111);color:var(--dsw-alias-bg-base,#fff)}
596
612
  .gkb-sk-foot-note{margin-left:auto;font-size:10px;color:var(--dsw-alias-label-tertiary,#8a939b);align-self:center}
613
+ .gkb-sk-head-actions{display:flex;align-items:center;gap:8px;flex:none}
614
+ .gkb-sk-cost{color:var(--dsw-alias-label-secondary,#7b8490)}
615
+ .gkb-sk-cost[data-heavy="true"]{color:#8E2B18;font-weight:650}
616
+ .gkb-sk-notice{align-self:flex-start;padding:6px 11px;border-radius:999px;border:1px solid color-mix(in srgb,#0E6E5E 28%,transparent);background:color-mix(in srgb,#0E6E5E 9%,transparent);color:color-mix(in srgb,#0E6E5E 80%,#111);font-size:11px;font-weight:600;animation:gkb-sk-in .2s ease-out}
617
+ .gkb-sk-btn[data-tone="danger"]{border-color:color-mix(in srgb,#8E2B18 40%,transparent);color:#8E2B18}
618
+ .gkb-sk-btn[data-tone="danger"]:hover{background:color-mix(in srgb,#8E2B18 8%,transparent)}
619
+ .gkb-sk-btn:disabled{opacity:.5;cursor:default}
620
+ .gkb-sk-editor{display:flex;flex-direction:column;gap:8px}
621
+ .gkb-sk-editor-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--dsw-alias-label-primary,#111)}
622
+ .gkb-sk-editor-cost{font-size:10.5px;font-weight:650;color:var(--dsw-alias-label-secondary,#7b8490);font-variant-numeric:tabular-nums}
623
+ .gkb-sk-editor-cost[data-heavy="true"]{color:#8E2B18}
624
+ .gkb-sk-editor-area{width:100%;box-sizing:border-box;min-height:320px;max-height:46vh;padding:12px 14px;border-radius:11px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:var(--dsw-alias-bg-layer-1,#f7f8f7);color:var(--dsw-alias-label-primary,#1f2937);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;line-height:17px;resize:vertical;tab-size:2;scrollbar-width:thin}
625
+ .gkb-sk-editor-area:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 55%,transparent);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 13%,transparent)}
626
+ .gkb-sk-editor-hint{font-size:10.5px;line-height:15px;color:var(--dsw-alias-label-secondary,#7b8490)}
627
+ .gkb-sk-modal-backdrop{position:fixed;inset:0;z-index:9020;display:flex;align-items:center;justify-content:center;padding:20px;background:color-mix(in srgb,#070b0a 40%,transparent);animation:gkb-sk-fade .16s ease-out}
628
+ .gkb-sk-modal{width:min(460px,92vw);display:flex;flex-direction:column;gap:12px;padding:20px;border-radius:16px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);box-shadow:0 32px 90px -40px rgba(0,10,8,.6);animation:gkb-sk-in .22s cubic-bezier(.16,1,.3,1)}
629
+ .gkb-sk-modal-title{margin:0;font-size:17px;line-height:23px;font-weight:720;letter-spacing:-.01em;color:var(--dsw-alias-label-primary,#111)}
630
+ .gkb-sk-modal-text{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472)}
631
+ .gkb-sk-modal-code{display:block;padding:8px 11px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f7f8f7);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary,#111)}
632
+ .gkb-sk-field{display:flex;flex-direction:column;gap:5px;font-size:10.5px;font-weight:650;letter-spacing:.04em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#7b8490)}
633
+ .gkb-sk-input{padding:9px 11px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,#f7f8f7);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;letter-spacing:0;text-transform:none}
634
+ .gkb-sk-input:focus,.gkb-sk-input:focus-visible{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 55%,transparent);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 13%,transparent)}
635
+ .gkb-sk-modal-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
636
+ .gkb-fab{position:fixed;right:16px;bottom:96px;z-index:8500;display:none;align-items:center;gap:8px;padding:10px 14px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:12px;font-weight:650;box-shadow:0 12px 32px -14px rgba(0,10,8,.45);cursor:pointer}
637
+ .gkb-fab b{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 5px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 12%,transparent);color:var(--dsw-alias-brand-primary,#0E6E5E);font-size:10px;font-weight:750}
638
+ .gkb-fab:active{transform:scale(.98)}
639
+ @media (max-width: 760px){.gkb-fab{display:inline-flex}}
597
640
  @media (max-width:760px){
598
641
  .gkb-sk-backdrop{padding:0}
599
642
  .gkb-sk-panel{width:100vw;height:100dvh;max-height:none;border-radius:0;border:none}
600
- .gkb-sk-head{padding:16px 16px 12px}
601
- .gkb-sk-title{font-size:20px;line-height:26px}
602
- .gkb-sk-body{padding:14px 16px 20px}
643
+ .gkb-sk-head{padding:14px 14px 10px;gap:9px}
644
+ .gkb-sk-head-row{flex-wrap:wrap;gap:8px}
645
+ .gkb-sk-head-row > div:first-child{flex:1 1 100%;min-width:0}
646
+ .gkb-sk-kicker{white-space:nowrap;font-size:9px;letter-spacing:.13em}
647
+ .gkb-sk-title{font-size:22px;line-height:27px;margin-top:4px}
648
+ .gkb-sk-sub{font-size:11px;line-height:16px}
649
+ .gkb-sk-head-actions{width:100%;justify-content:flex-start;gap:7px}
650
+ .gkb-sk-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
651
+ .gkb-sk-body{padding:12px 14px 20px}
603
652
  .gkb-sk-grid,.gkb-sk-skeleton{grid-template-columns:1fr}
604
653
  .gkb-sk-card-desc{min-height:0;-webkit-line-clamp:4}
605
654
  .gkb-sk-detail{width:100%;max-height:100%}
606
655
  .gkb-sk-detail-body{padding:14px 14px 16px}
607
656
  .gkb-sk-detail-head{padding:14px 14px 12px}
608
657
  .gkb-sk-detail-foot{padding:10px 14px 14px}
658
+ .gkb-sk-editor-area{min-height:240px;max-height:40vh}
609
659
  }
610
660
  @media (prefers-color-scheme:dark){
611
661
  .gkb-sk-panel{box-shadow:0 48px 140px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.05)}
@@ -670,25 +720,21 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
670
720
  h("div", { key: "titles" }, [
671
721
  h("div", { key: "label", style: LABEL_STYLE }, "Навыки"),
672
722
  h("div", { key: "sub", style: SKILLS_SUB_STYLE }, totals
673
- ? pluralRuns(totals.runs) + " · " + totals.used + " из " + totals.skills + " использованы"
723
+ ? pluralRuns(totals.runs) + " · " + totals.used + "/" + totals.skills + " · ≈" + compactTokens(totals.tokens)
674
724
  : "загрузка статистики…")
675
725
  ]),
676
726
  h("span", { key: "count", style: SKILLS_COUNT_STYLE }, String(count))
677
727
  ]),
678
728
  active.length > 0
679
- ? h("div", { key: "active", style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "6px", minWidth: 0 } }, [
729
+ ? h("div", { key: "active", className: "gkb-skills-active", style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "6px", minWidth: 0 } }, [
680
730
  h("span", { key: "dot", className: "gkb-sk-pulse", style: { width: "6px", height: "6px", boxShadow: "0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #0E6E5E) 15%, transparent)" } }),
681
731
  h("span", { key: "label", style: { fontSize: "9px", fontWeight: 750, letterSpacing: ".12em", textTransform: "uppercase", color: "var(--dsw-alias-brand-primary, #0E6E5E)", flex: "none" } }, "в работе"),
682
732
  h("span", { key: "names", style: { fontSize: "10px", color: "var(--dsw-alias-label-secondary, #666)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, activeNames.join(", ") + activeMore)
683
733
  ])
684
734
  : null,
685
- h("div", { key: "chips", style: SKILLS_CHIPS_STYLE }, categories.map((category) =>
735
+ h("div", { key: "chips", className: "gkb-skills-chips", style: SKILLS_CHIPS_STYLE }, categories.slice(0, 4).map((category) =>
686
736
  h("span", { key: category.id, style: SKILLS_CHIP_STYLE }, category.label + " " + (counts[category.id] || ""))
687
- )),
688
- h("div", { key: "footer", style: { marginTop: "7px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", fontSize: "10px", fontWeight: 650, letterSpacing: ".04em", color: "var(--dsw-alias-label-secondary, #777)" } }, [
689
- h("span", { key: "text" }, "Открыть каталог"),
690
- h("span", { key: "arrow", style: { opacity: .8 } }, "→")
691
- ])
737
+ ))
692
738
  ]);
693
739
  }
694
740
 
@@ -702,6 +748,81 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
702
748
  const [detailLoading, setDetailLoading] = react.useState(false);
703
749
  const [detailError, setDetailError] = react.useState("");
704
750
  const [copied, setCopied] = react.useState(false);
751
+ const [editor, setEditor] = react.useState(null);
752
+ const [createDraft, setCreateDraft] = react.useState(null);
753
+ const [deleteConfirm, setDeleteConfirm] = react.useState(null);
754
+ const [notice, setNotice] = react.useState("");
755
+ const editorRef = react.useRef(null);
756
+ react.useEffect(() => {
757
+ if (editor && editorRef.current) editorRef.current.focus();
758
+ }, [editor && editor.name]);
759
+
760
+ const tokenEstimate = (text) => {
761
+ try { return Math.max(40, Math.round(new TextEncoder().encode(String(text)).length / 3.2)); } catch { return 0; }
762
+ };
763
+ const shortTokens = (value) => {
764
+ const n = Number(value) || 0;
765
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "").replace(".", ",") + "k";
766
+ return String(n);
767
+ };
768
+ const flash = (message) => {
769
+ setNotice(message);
770
+ setTimeout(() => setNotice(""), 3200);
771
+ };
772
+ const reloadSkills = async () => {
773
+ await loadSkills(store.sessionId, true);
774
+ };
775
+ const openEditor = () => {
776
+ if (!detail || !detail.skill) return;
777
+ setEditor({ name: detail.skill.name, content: detail.skill.content, saving: false, error: "", baseTokens: detail.skill.tokens });
778
+ };
779
+ const saveEditor = async () => {
780
+ if (!editor || editor.saving) return;
781
+ setEditor(Object.assign({}, editor, { saving: true, error: "" }));
782
+ try {
783
+ const saved = await callSkills("skill-save", { name: editor.name, content: editor.content, sessionId: store.sessionId || undefined });
784
+ setDetail((previous) => previous ? Object.assign({}, previous, { skill: saved }) : previous);
785
+ setEditor(null);
786
+ await reloadSkills();
787
+ flash("Сохранено · резервная копия в .dsh-kit-backups");
788
+ } catch (error) {
789
+ setEditor((previous) => previous ? Object.assign({}, previous, { saving: false, error: String(error && error.message ? error.message : error) }) : previous);
790
+ }
791
+ };
792
+ const submitCreate = async () => {
793
+ if (!createDraft || createDraft.busy) return;
794
+ const name = createDraft.name.trim();
795
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
796
+ setCreateDraft(Object.assign({}, createDraft, { error: "Имя: kebab-case из латиницы и цифр, например my-skill" }));
797
+ return;
798
+ }
799
+ setCreateDraft(Object.assign({}, createDraft, { busy: true, error: "" }));
800
+ try {
801
+ const result = createDraft.mode === "duplicate"
802
+ ? await callSkills("skill-duplicate", { source: createDraft.source, name, sessionId: store.sessionId || undefined })
803
+ : await callSkills("skill-create", { name, description: createDraft.description.trim(), sessionId: store.sessionId || undefined });
804
+ setCreateDraft(null);
805
+ await reloadSkills();
806
+ setSelected(result.skill.name);
807
+ flash(createDraft.mode === "duplicate" ? "Копия создана" : "Навык создан");
808
+ } catch (error) {
809
+ setCreateDraft((previous) => previous ? Object.assign({}, previous, { busy: false, error: String(error && error.message ? error.message : error) }) : previous);
810
+ }
811
+ };
812
+ const submitDelete = async () => {
813
+ if (!deleteConfirm || deleteConfirm.busy) return;
814
+ setDeleteConfirm(Object.assign({}, deleteConfirm, { busy: true, error: "" }));
815
+ try {
816
+ await callSkills("skill-delete", { name: deleteConfirm.name, sessionId: store.sessionId || undefined });
817
+ setDeleteConfirm(null);
818
+ setSelected(null);
819
+ setDetail(null);
820
+ await reloadSkills();
821
+ flash("Навык перемещён в .dsh-kit-trash — данные не потеряны");
822
+ } catch (error) {
823
+ setDeleteConfirm((previous) => previous ? Object.assign({}, previous, { busy: false, error: String(error && error.message ? error.message : error) }) : previous);
824
+ }
825
+ };
705
826
 
706
827
  react.useEffect(() => {
707
828
  if (store.open) void loadSkills(store.sessionId, true);
@@ -714,14 +835,22 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
714
835
  react.useEffect(() => {
715
836
  if (!store.open) return undefined;
716
837
  const onKey = (event) => {
838
+ if ((event.metaKey || event.ctrlKey) && String(event.key).toLowerCase() === "s" && editor) {
839
+ event.preventDefault();
840
+ void saveEditor();
841
+ return;
842
+ }
717
843
  if (event.key !== "Escape") return;
718
844
  event.stopPropagation();
845
+ if (editor) { setEditor(null); return; }
846
+ if (deleteConfirm) { setDeleteConfirm(null); return; }
847
+ if (createDraft) { setCreateDraft(null); return; }
719
848
  if (selected) { setSelected(null); setDetail(null); }
720
849
  else skillsStore.set({ open: false });
721
850
  };
722
851
  document.addEventListener("keydown", onKey, true);
723
852
  return () => document.removeEventListener("keydown", onKey, true);
724
- }, [store.open, selected]);
853
+ }, [store.open, selected, editor, deleteConfirm, createDraft]);
725
854
  react.useEffect(() => {
726
855
  if (!selected) { setDetail(null); setDetailError(""); return; }
727
856
  let alive = true;
@@ -786,7 +915,8 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
786
915
  h("span", { className: "gkb-sk-card-foot", key: "foot" }, [
787
916
  h("span", { className: "gkb-sk-meta", key: "meta" }, [
788
917
  h("span", { key: "files" }, h("b", null, "обновлён "), shortDate(skill.updatedAt)),
789
- h("span", { key: "used" }, h("b", null, skill.usage.count > 0 ? "последний раз " : "ещё не использовался"), skill.usage.count > 0 ? shortDate(skill.usage.lastAt) : "")
918
+ h("span", { key: "used" }, h("b", null, skill.usage.count > 0 ? "последний раз " : "ещё не использовался"), skill.usage.count > 0 ? shortDate(skill.usage.lastAt) : ""),
919
+ h("span", { key: "cost", className: "gkb-sk-cost", "data-heavy": skill.heavy ? "true" : "false" }, "ядро ≈" + shortTokens(skill.tokens))
790
920
  ]),
791
921
  activeNames.has(skill.name) ? h("span", { className: "gkb-sk-now", key: "spark", style: { position: "static" } }, "в работе") : h("span", { className: "gkb-sk-spark", key: "spark" }, sparkline(skill.usage.series, skill.color))
792
922
  ]),
@@ -812,6 +942,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
812
942
  h("div", { className: "gkb-sk-stat", key: "week" }, [h("span", { key: "l" }, "за неделю"), h("b", { key: "v" }, String(detail.usage ? detail.usage.week : 0))]),
813
943
  h("div", { className: "gkb-sk-stat", key: "last" }, [h("span", { key: "l" }, "последний"), h("b", { key: "v" }, shortDate(detail.usage && detail.usage.lastAt)), h("small", { key: "s" }, detail.usage && detail.usage.lastAt ? new Date(detail.usage.lastAt).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }) : "")]),
814
944
  h("div", { className: "gkb-sk-stat", key: "files" }, [h("span", { key: "l" }, "файлов"), h("b", { key: "v" }, String(detail.skill.files)), h("small", { key: "s" }, detail.skill.human)]),
945
+ h("div", { className: "gkb-sk-stat", key: "tokens" }, [h("span", { key: "l" }, "ядро"), h("b", { key: "v" }, "≈" + shortTokens(detail.skill.tokens)), h("small", { key: "s" }, detail.skill.heavy ? "выше бюджета 6k" : "в бюджете")]),
815
946
  h("div", { className: "gkb-sk-stat", key: "updated" }, [h("span", { key: "l" }, "обновлён"), h("b", { key: "v" }, shortDate(detail.skill.updatedAt))])
816
947
  ])
817
948
  : null,
@@ -819,11 +950,46 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
819
950
  ? h("div", { key: "when", className: "gkb-sk-when" }, [h("b", { key: "b" }, "Когда применять: "), detail.skill.whenToUse])
820
951
  : null,
821
952
  detail && detail.skill
822
- ? h("pre", { key: "content", className: "gkb-sk-content" }, detail.skill.content)
953
+ ? (editor
954
+ ? h("div", { key: "editor", className: "gkb-sk-editor" }, [
955
+ h("div", { key: "head", className: "gkb-sk-editor-head" }, [
956
+ h("span", { key: "t" }, "Редактирование SKILL.md"),
957
+ h("span", { key: "c", className: "gkb-sk-editor-cost", "data-heavy": tokenEstimate(editor.content) > 6000 ? "true" : "false" }, "≈" + shortTokens(tokenEstimate(editor.content)) + " токенов")
958
+ ]),
959
+ editor.error ? h("div", { key: "err", className: "gkb-sk-error" }, editor.error) : null,
960
+ h("textarea", {
961
+ key: "area",
962
+ ref: editorRef,
963
+ className: "gkb-sk-editor-area",
964
+ value: editor.content,
965
+ spellCheck: false,
966
+ "aria-label": "Содержимое SKILL.md",
967
+ onChange: (event) => setEditor(Object.assign({}, editor, { content: event.target.value })),
968
+ onKeyDown: (event) => {
969
+ if (event.key !== "Tab") return;
970
+ event.preventDefault();
971
+ const element = event.target;
972
+ const start = element.selectionStart;
973
+ const end = element.selectionEnd;
974
+ const next = editor.content.slice(0, start) + " " + editor.content.slice(end);
975
+ setEditor(Object.assign({}, editor, { content: next }));
976
+ requestAnimationFrame(() => { element.selectionStart = start + 2; element.selectionEnd = start + 2; });
977
+ }
978
+ }),
979
+ h("div", { key: "hint", className: "gkb-sk-editor-hint" }, "Ctrl+S — сохранить. Перед записью создаётся резервная копия в ~/.dsh/skills/.dsh-kit-backups. Держите ядро ≤ 6k токенов: детали — в references.")
980
+ ])
981
+ : h("pre", { key: "content", className: "gkb-sk-content" }, detail.skill.content))
823
982
  : null
824
983
  ]),
825
- h("div", { className: "gkb-sk-detail-foot" }, [
826
- h("button", { key: "open", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: () => { void callSkills("open-skill", { name: selected }).catch(() => {}); } }, "Открыть папку"),
984
+ h("div", { className: "gkb-sk-detail-foot" }, editor ? [
985
+ h("button", { key: "save", type: "button", className: "gkb-sk-btn", "data-tone": "primary", disabled: editor.saving, onClick: () => { void saveEditor(); } }, editor.saving ? "Сохраняю…" : "Сохранить"),
986
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setEditor(null) }, "Отмена"),
987
+ h("span", { key: "note", className: "gkb-sk-foot-note" }, "≈" + shortTokens(tokenEstimate(editor.content)) + " токенов · Ctrl+S")
988
+ ] : [
989
+ h("button", { key: "edit", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: openEditor }, "Редактировать"),
990
+ h("button", { key: "dup", type: "button", className: "gkb-sk-btn", onClick: () => setCreateDraft({ mode: "duplicate", name: selected + "-copy", source: selected, description: "", error: "" }) }, "Дублировать"),
991
+ h("button", { key: "del", type: "button", className: "gkb-sk-btn", "data-tone": "danger", onClick: () => setDeleteConfirm({ name: selected, error: "" }) }, "Удалить"),
992
+ h("button", { key: "open", type: "button", className: "gkb-sk-btn", onClick: () => { void callSkills("open-skill", { name: selected }).catch(() => {}); } }, "Открыть папку"),
827
993
  h("button", { key: "copy", type: "button", className: "gkb-sk-btn", onClick: () => { try { void navigator.clipboard.writeText(selected); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch {} } }, copied ? "Скопировано" : "Скопировать имя"),
828
994
  h("button", { key: "close", type: "button", className: "gkb-sk-btn", onClick: () => { setSelected(null); setDetail(null); } }, "Закрыть"),
829
995
  h("span", { key: "path", className: "gkb-sk-foot-note" }, detail && detail.skill ? detail.skill.path : "")
@@ -832,6 +998,47 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
832
998
  )
833
999
  : null;
834
1000
 
1001
+ const createModal = createDraft
1002
+ ? h("div", { className: "gkb-sk-modal-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) setCreateDraft(null); } },
1003
+ h("div", { className: "gkb-sk-modal", role: "dialog", "aria-modal": "true", "aria-label": createDraft.mode === "duplicate" ? "Дублировать навык" : "Новый навык" }, [
1004
+ h("h3", { key: "title", className: "gkb-sk-modal-title" }, createDraft.mode === "duplicate" ? "Дублировать навык" : "Новый навык"),
1005
+ createDraft.mode === "duplicate"
1006
+ ? h("p", { key: "text", className: "gkb-sk-modal-text" }, "Копия навыка «" + createDraft.source + "» со всеми references.")
1007
+ : h("p", { key: "text", className: "gkb-sk-modal-text" }, "Появится папка в ~/.dsh/skills с компактным шаблоном: ядро ≤ 6k токенов, детали — в references."),
1008
+ h("label", { key: "name", className: "gkb-sk-field" }, [
1009
+ h("span", { key: "s" }, "Имя (kebab-case)"),
1010
+ h("input", { key: "i", className: "gkb-sk-input", value: createDraft.name, autoFocus: true, placeholder: "my-skill", onChange: (event) => setCreateDraft(Object.assign({}, createDraft, { name: event.target.value })) })
1011
+ ]),
1012
+ createDraft.mode === "create"
1013
+ ? h("label", { key: "desc", className: "gkb-sk-field" }, [
1014
+ h("span", { key: "s" }, "Короткое описание-триггер"),
1015
+ h("input", { key: "i", className: "gkb-sk-input", value: createDraft.description, placeholder: "Когда применять навык — одной строкой", onChange: (event) => setCreateDraft(Object.assign({}, createDraft, { description: event.target.value })) })
1016
+ ])
1017
+ : null,
1018
+ createDraft.error ? h("div", { key: "error", className: "gkb-sk-error" }, createDraft.error) : null,
1019
+ h("div", { key: "foot", className: "gkb-sk-modal-foot" }, [
1020
+ h("button", { key: "ok", type: "button", className: "gkb-sk-btn", "data-tone": "primary", disabled: createDraft.busy, onClick: () => { void submitCreate(); } }, createDraft.busy ? "Создаю…" : (createDraft.mode === "duplicate" ? "Создать копию" : "Создать навык")),
1021
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setCreateDraft(null) }, "Отмена")
1022
+ ])
1023
+ ])
1024
+ )
1025
+ : null;
1026
+
1027
+ const deleteModal = deleteConfirm
1028
+ ? h("div", { className: "gkb-sk-modal-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) setDeleteConfirm(null); } },
1029
+ h("div", { className: "gkb-sk-modal", role: "dialog", "aria-modal": "true", "aria-label": "Удалить навык" }, [
1030
+ h("h3", { key: "title", className: "gkb-sk-modal-title" }, "Удалить навык?"),
1031
+ h("p", { key: "text", className: "gkb-sk-modal-text" }, "Папка переместится в ~/.dsh/skills/.dsh-kit-trash — это обратимо, файлы не удаляются."),
1032
+ h("code", { key: "name", className: "gkb-sk-modal-code" }, deleteConfirm.name),
1033
+ deleteConfirm.error ? h("div", { key: "error", className: "gkb-sk-error" }, deleteConfirm.error) : null,
1034
+ h("div", { key: "foot", className: "gkb-sk-modal-foot" }, [
1035
+ h("button", { key: "ok", type: "button", className: "gkb-sk-btn", "data-tone": "danger", disabled: deleteConfirm.busy, onClick: () => { void submitDelete(); } }, deleteConfirm.busy ? "Переношу…" : "В корзину"),
1036
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setDeleteConfirm(null) }, "Отмена")
1037
+ ])
1038
+ ])
1039
+ )
1040
+ : null;
1041
+
835
1042
  return h("div", { className: "gkb-sk-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) close(); } },
836
1043
  h("div", { className: "gkb-sk-panel", role: "dialog", "aria-modal": "true", "aria-label": "Каталог навыков" },
837
1044
  h("div", { className: "gkb-sk-head" }, [
@@ -840,10 +1047,14 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
840
1047
  h("div", { key: "kicker", className: "gkb-sk-kicker" }, "DSH · навыки"),
841
1048
  h("h2", { key: "title", className: "gkb-sk-title" }, "Мои навыки"),
842
1049
  h("div", { key: "sub", className: "gkb-sk-sub" }, total
843
- ? [h("b", { key: "n" }, pluralSkills(total.skills)), " · ", h("b", { key: "r" }, pluralRuns(total.runs)), " · ", h("b", { key: "u" }, String(total.used)), " из ", String(total.skills), " использованы", total.active > 0 ? " · " + total.active + " в работе" : ""]
1050
+ ? [h("b", { key: "n" }, pluralSkills(total.skills)), " · ", h("b", { key: "r" }, pluralRuns(total.runs)), " · ", h("b", { key: "u" }, String(total.used)), " из ", String(total.skills), " использовано", " · ≈", h("b", { key: "t" }, shortTokens(total.tokens)), " токенов ядер", total.active > 0 ? " · " + total.active + " в работе" : ""]
844
1051
  : "Загружаю статистику…")
845
1052
  ]),
846
- h("button", { key: "close", type: "button", className: "gkb-sk-close", onClick: close, "aria-label": "Закрыть каталог навыков" }, "×")
1053
+ h("div", { key: "actions", className: "gkb-sk-head-actions" }, [
1054
+ h("button", { key: "tokens", type: "button", className: "gkb-sk-btn", onClick: () => { try { if (typeof window.__DSH_TOKEN_OPEN__ === "function") window.__DSH_TOKEN_OPEN__(store.sessionId || undefined); } catch {} } }, "Токены"),
1055
+ h("button", { key: "new", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: () => setCreateDraft({ mode: "create", name: "", description: "", error: "" }) }, "Новый навык"),
1056
+ h("button", { key: "close", type: "button", className: "gkb-sk-close", onClick: close, "aria-label": "Закрыть каталог навыков" }, "×")
1057
+ ])
847
1058
  ]),
848
1059
  h("div", { key: "search", className: "gkb-sk-search-row" }, [
849
1060
  h("input", { key: "input", className: "gkb-sk-search", placeholder: "Поиск по названию, описанию или категории…", value: query, onChange: (event) => setQuery(event.target.value), autoFocus: true, "aria-label": "Поиск навыков" }),
@@ -855,6 +1066,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
855
1066
  ])
856
1067
  ]),
857
1068
  activeBand,
1069
+ notice ? h("div", { key: "notice", className: "gkb-sk-notice", role: "status" }, notice) : null,
858
1070
  h("div", { key: "filters", className: "gkb-sk-filters" }, [
859
1071
  h("button", { key: "all", type: "button", className: "gkb-sk-chip", "data-active": category === "all" ? "true" : "false", onClick: () => setCategory("all") }, "Все " + skills.length),
860
1072
  ...categories.filter((item) => counts[item.id] > 0).map((item) => h("button", {
@@ -879,10 +1091,36 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
879
1091
  : null
880
1092
  ]),
881
1093
  detailPanel
882
- )
1094
+ ),
1095
+ createModal,
1096
+ deleteModal
883
1097
  );
884
1098
  }
885
1099
 
1100
+ /* Mobile rail cannot hold the tile stack; one pill opens the catalog. */
1101
+ function MobileSkillsFab() {
1102
+ const store = useSkillsStore();
1103
+ if (store.open) return null;
1104
+ const totals = store.data ? store.data.totals : null;
1105
+ const open = () => {
1106
+ skillsStore.set({ open: true, sessionId: store.sessionId || null });
1107
+ void loadSkills(store.sessionId, true);
1108
+ };
1109
+ return h("button", {
1110
+ type: "button",
1111
+ className: "gkb-fab",
1112
+ onClick: open,
1113
+ "aria-label": "Открыть каталог навыков"
1114
+ }, [
1115
+ h("svg", { key: "icon", width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" }, [
1116
+ h("path", { key: "a", d: "M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2L12 3z", stroke: "currentColor", strokeWidth: "1.6", strokeLinejoin: "round" }),
1117
+ h("circle", { key: "b", cx: 19, cy: 5, r: 1.4, fill: "currentColor" })
1118
+ ]),
1119
+ h("span", { key: "label" }, "Навыки"),
1120
+ totals ? h("b", { key: "count" }, String(totals.skills)) : null
1121
+ ]);
1122
+ }
1123
+
886
1124
  function SpendRow(props) {
887
1125
  if (props && props.wide === false) return null;
888
1126
  const snap = useSnapshot();
@@ -908,13 +1146,16 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
908
1146
  h("span", { key: "value", style: amountStyle }, value)
909
1147
  ]);
910
1148
  }
911
- /* One slot registration: two stacked full-width AURORA tiles */
1149
+ /* One slot registration: AURORA tiles in a single full-width column.
1150
+ * Token Lab (a sibling client part) hands its tile over through a global;
1151
+ * rendering it here keeps the footer stack instead of a second slot row. */
912
1152
  function SidebarTiles(props) {
1153
+ const TokenTile = typeof window !== "undefined" ? window.__DSH_TOKEN_TILE__ : null;
913
1154
  return h("div", {
914
1155
  style: {
915
1156
  display: "flex",
916
1157
  flexDirection: "column",
917
- gap: "2px",
1158
+ gap: "4px",
918
1159
  width: "100%",
919
1160
  boxSizing: "border-box",
920
1161
  padding: "4px 8px 6px"
@@ -922,6 +1163,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
922
1163
  "data-gkb-tiles": "1"
923
1164
  }, [
924
1165
  h(SkillsRow, Object.assign({ key: "skills" }, props)),
1166
+ TokenTile ? h(TokenTile, { key: "tokens", useSessions: props && props.useSessions }) : null,
925
1167
  h(BalanceRow, { key: "balance" }),
926
1168
  h(SpendRow, { key: "spend" })
927
1169
  ]);
@@ -1074,6 +1316,12 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1074
1316
  order: 640
1075
1317
  }, SkillsCatalog)), "gkb-ru: skills catalog overlay");
1076
1318
 
1319
+ ctx.effect(() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1320
+ name: "shell.overlay",
1321
+ id: "gkb-mobile-fab",
1322
+ order: 660
1323
+ }, MobileSkillsFab)), "gkb-ru: mobile skills fab");
1324
+
1077
1325
  }
1078
1326
 
1079
1327
  exports.apply = apply;
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { spawn } from "node:child_process";
14
- import { collectSkillsData, listSkills, readSkillContent, skillsHome } from "./skills.js";
14
+ import { collectSkillsData, listSkills, readSkillContent, skillsHome, saveSkillContent, createSkill, duplicateSkill, deleteSkill } from "./skills.js";
15
15
 
16
16
  const ROUTE_BASE = "/api/dsh-gkb-ru-ui";
17
17
  const RPC_NAMESPACE = "dsh-gkb-ru-ui";
@@ -127,6 +127,50 @@ export function apply(ctx) {
127
127
  return { skill, usage };
128
128
  });
129
129
 
130
+ registerOperation(ctx, "skill-save", async (payload) => {
131
+ const name = typeof payload?.name === "string" ? payload.name : "";
132
+ const content = typeof payload?.content === "string" ? payload.content : "";
133
+ if (name.length === 0) {
134
+ const error = new Error("не указано имя навыка");
135
+ error.code = "skills/bad-request";
136
+ throw error;
137
+ }
138
+ const saved = saveSkillContent(skillsHome(), name, content);
139
+ const data = collectSkillsData(skillsHome(), typeof payload?.sessionId === "string" ? payload.sessionId : null);
140
+ return { ...saved, totals: data.totals };
141
+ });
142
+
143
+ registerOperation(ctx, "skill-create", async (payload) => {
144
+ const skill = createSkill(skillsHome(), payload);
145
+ const data = collectSkillsData(skillsHome(), typeof payload?.sessionId === "string" ? payload.sessionId : null);
146
+ return { skill, totals: data.totals };
147
+ });
148
+
149
+ registerOperation(ctx, "skill-duplicate", async (payload) => {
150
+ const source = typeof payload?.source === "string" ? payload.source : "";
151
+ const name = typeof payload?.name === "string" ? payload.name : "";
152
+ if (source.length === 0) {
153
+ const error = new Error("не указан исходный навык");
154
+ error.code = "skills/bad-request";
155
+ throw error;
156
+ }
157
+ const skill = duplicateSkill(skillsHome(), source, name);
158
+ const data = collectSkillsData(skillsHome(), typeof payload?.sessionId === "string" ? payload.sessionId : null);
159
+ return { skill, totals: data.totals };
160
+ });
161
+
162
+ registerOperation(ctx, "skill-delete", async (payload) => {
163
+ const name = typeof payload?.name === "string" ? payload.name : "";
164
+ if (name.length === 0) {
165
+ const error = new Error("не указано имя навыка");
166
+ error.code = "skills/bad-request";
167
+ throw error;
168
+ }
169
+ const result = deleteSkill(skillsHome(), name);
170
+ const data = collectSkillsData(skillsHome(), typeof payload?.sessionId === "string" ? payload.sessionId : null);
171
+ return { ...result, totals: data.totals };
172
+ });
173
+
130
174
  registerOperation(ctx, "open-skill", async (payload) => {
131
175
  const name = typeof payload?.name === "string" ? payload.name : "";
132
176
  const home = skillsHome();