dsh-desktop-kit 1.0.0 → 2.0.0

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 +84 -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
package/client.js CHANGED
@@ -88,6 +88,11 @@ window.__ModuleLoader__.load({
88
88
  if (value % 10 >= 2 && value % 10 <= 4 && (value % 100 < 12 || value % 100 > 14)) return value + " запуска";
89
89
  return value + " запусков";
90
90
  }
91
+ function compactTokens(value) {
92
+ const n = Number(value) || 0;
93
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "").replace(".", ",") + "k";
94
+ return String(n);
95
+ }
91
96
  function shortDate(iso) {
92
97
  if (!iso) return "—";
93
98
  try {
@@ -448,7 +453,7 @@ window.__ModuleLoader__.load({
448
453
  { id: "design", label: "Дизайн", color: "#0E6E5E", skills: ["wow-design", "design-critique", "ui-design-system"] },
449
454
  { id: "seo", label: "SEO", color: "#2C5F8A", skills: ["seo"] },
450
455
  { id: "content", label: "Контент", color: "#B36A43", skills: ["content-writing"] },
451
- { id: "process", label: "Процесс", color: "#6B7275", skills: ["dev-workflow", "requirements-and-planning", "skills-router"] },
456
+ { id: "process", label: "Процесс", color: "#6B7275", skills: ["dev-workflow", "requirements-and-planning", "skills-router", "context-economy"] },
452
457
  { id: "quality", label: "Качество", color: "#1F7A57", skills: ["testing-strategy", "debugging-playbook", "code-review-checklist"] },
453
458
  { id: "safety", label: "Данные", color: "#94661C", skills: ["database-safety", "security-review", "pg-upgrade-verify"] },
454
459
  { id: "release", label: "Релиз", color: "#2C5F8A", skills: ["release-and-deploy", "git-and-ci"] },
@@ -456,7 +461,7 @@ window.__ModuleLoader__.load({
456
461
  { id: "memory", label: "Память", color: "#5B4B8A", skills: ["project-deep-dive", "project-memory-maintenance"] }
457
462
  ];
458
463
  const SKILL_NAMES = SKILL_GROUPS.reduce((acc, group) => acc.concat(group.skills), []);
459
- const SKILLS_TILE_STYLE = Object.assign({}, TILE_BASE, { display: "block", padding: "10px 12px", cursor: "pointer" });
464
+ const SKILLS_TILE_STYLE = Object.assign({}, TILE_BASE, { display: "block", padding: "8px 10px", cursor: "pointer" });
460
465
  const SKILLS_HEAD_STYLE = { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "8px" };
461
466
  const SKILLS_COUNT_STYLE = {
462
467
  display: "inline-flex", alignItems: "center", justifyContent: "center",
@@ -469,14 +474,14 @@ window.__ModuleLoader__.load({
469
474
  fontSize: "10px", fontWeight: 500, marginTop: "2px", letterSpacing: "0.02em",
470
475
  color: "var(--dsw-alias-label-secondary, #777)"
471
476
  };
472
- const SKILLS_CHIPS_STYLE = { display: "flex", flexWrap: "wrap", gap: "4px", marginTop: "8px" };
477
+ 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%)" };
473
478
  const SKILLS_CHIP_STYLE = {
474
- fontSize: "10px", fontWeight: 600, lineHeight: 1.5,
479
+ fontSize: "9.5px", fontWeight: 600, lineHeight: 1.45,
475
480
  color: "var(--dsw-alias-label-secondary, #666)",
476
481
  border: "1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.10))",
477
482
  borderRadius: "999px", padding: "1px 6px",
478
483
  background: "var(--dsw-alias-settings-card-fill, rgba(0,0,0,.03))",
479
- whiteSpace: "nowrap"
484
+ whiteSpace: "nowrap", flex: "0 0 auto"
480
485
  };
481
486
  const SKILLS_TOGGLE_STYLE = {
482
487
  marginTop: "8px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px",
@@ -540,10 +545,20 @@ window.__ModuleLoader__.load({
540
545
  .wtg9ga_root .GAkraW_groupSection.gkb-collapsed > .wnFoaW_projectRow::after { content: "▸"; }
541
546
  .wtg9ga_root .GAkraW_groupSection.gkb-collapsed > span:not(:first-child) { display: none !important; }
542
547
  .wtg9ga_root .wnFoaW_projectRow:hover::after { opacity: 1; color: var(--dsw-alias-label-primary, #111); }
548
+ /* Short windows: the sidebar must still show the account/settings foot. */
549
+ @media (max-height: 820px) {
550
+ .wtg9ga_root > .wtg9ga_regionArea { min-height: 140px; }
551
+ .gkb-skills-chips { display: none !important; }
552
+ .gkb-skills-active { margin-top: 4px !important; }
553
+ }
554
+ /* Mobile rail is 55px: tiles cannot fit, so they live behind the FAB. */
555
+ @media (max-width: 760px) {
556
+ [data-gkb-tiles] { display: none !important; }
557
+ }
543
558
  `;
544
559
  const SKILLS_CATALOG_CSS = `
545
560
  .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}
546
- .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)}
561
+ .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)}
547
562
  .gkb-sk-head{flex:none;display:flex;flex-direction:column;gap:12px;padding:22px 24px 14px;background:
548
563
  radial-gradient(120% 160% at 100% 0%,color-mix(in srgb,var(--dsw-alias-brand-primary,#0E6E5E) 10%,transparent) 0%,transparent 55%),
549
564
  linear-gradient(180deg,color-mix(in srgb,var(--dsw-alias-bg-layer-1,#f6f7f6) 70%,transparent) 0%,transparent 100%);
@@ -556,7 +571,8 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
556
571
  .gkb-sk-sub b{color:var(--dsw-alias-label-primary,#111);font-weight:650}
557
572
  .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}
558
573
  .gkb-sk-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary,#111)}
559
- .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}
574
+ .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}
575
+ .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)}
560
576
  .gkb-sk-search-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
561
577
  .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}
562
578
  .gkb-sk-search::placeholder{color:var(--dsw-alias-label-tertiary,#98a0a8)}
@@ -612,18 +628,52 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
612
628
  .gkb-sk-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
613
629
  .gkb-sk-btn[data-tone="primary"]{border-color:transparent;background:var(--dsw-alias-label-primary,#111);color:var(--dsw-alias-bg-base,#fff)}
614
630
  .gkb-sk-foot-note{margin-left:auto;font-size:10px;color:var(--dsw-alias-label-tertiary,#8a939b);align-self:center}
631
+ .gkb-sk-head-actions{display:flex;align-items:center;gap:8px;flex:none}
632
+ .gkb-sk-cost{color:var(--dsw-alias-label-secondary,#7b8490)}
633
+ .gkb-sk-cost[data-heavy="true"]{color:#8E2B18;font-weight:650}
634
+ .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}
635
+ .gkb-sk-btn[data-tone="danger"]{border-color:color-mix(in srgb,#8E2B18 40%,transparent);color:#8E2B18}
636
+ .gkb-sk-btn[data-tone="danger"]:hover{background:color-mix(in srgb,#8E2B18 8%,transparent)}
637
+ .gkb-sk-btn:disabled{opacity:.5;cursor:default}
638
+ .gkb-sk-editor{display:flex;flex-direction:column;gap:8px}
639
+ .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)}
640
+ .gkb-sk-editor-cost{font-size:10.5px;font-weight:650;color:var(--dsw-alias-label-secondary,#7b8490);font-variant-numeric:tabular-nums}
641
+ .gkb-sk-editor-cost[data-heavy="true"]{color:#8E2B18}
642
+ .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}
643
+ .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)}
644
+ .gkb-sk-editor-hint{font-size:10.5px;line-height:15px;color:var(--dsw-alias-label-secondary,#7b8490)}
645
+ .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}
646
+ .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)}
647
+ .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)}
648
+ .gkb-sk-modal-text{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472)}
649
+ .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)}
650
+ .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)}
651
+ .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}
652
+ .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)}
653
+ .gkb-sk-modal-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
654
+ .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}
655
+ .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}
656
+ .gkb-fab:active{transform:scale(.98)}
657
+ @media (max-width: 760px){.gkb-fab{display:inline-flex}}
615
658
  @media (max-width:760px){
616
659
  .gkb-sk-backdrop{padding:0}
617
660
  .gkb-sk-panel{width:100vw;height:100dvh;max-height:none;border-radius:0;border:none}
618
- .gkb-sk-head{padding:16px 16px 12px}
619
- .gkb-sk-title{font-size:20px;line-height:26px}
620
- .gkb-sk-body{padding:14px 16px 20px}
661
+ .gkb-sk-head{padding:14px 14px 10px;gap:9px}
662
+ .gkb-sk-head-row{flex-wrap:wrap;gap:8px}
663
+ .gkb-sk-head-row > div:first-child{flex:1 1 100%;min-width:0}
664
+ .gkb-sk-kicker{white-space:nowrap;font-size:9px;letter-spacing:.13em}
665
+ .gkb-sk-title{font-size:22px;line-height:27px;margin-top:4px}
666
+ .gkb-sk-sub{font-size:11px;line-height:16px}
667
+ .gkb-sk-head-actions{width:100%;justify-content:flex-start;gap:7px}
668
+ .gkb-sk-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
669
+ .gkb-sk-body{padding:12px 14px 20px}
621
670
  .gkb-sk-grid,.gkb-sk-skeleton{grid-template-columns:1fr}
622
671
  .gkb-sk-card-desc{min-height:0;-webkit-line-clamp:4}
623
672
  .gkb-sk-detail{width:100%;max-height:100%}
624
673
  .gkb-sk-detail-body{padding:14px 14px 16px}
625
674
  .gkb-sk-detail-head{padding:14px 14px 12px}
626
675
  .gkb-sk-detail-foot{padding:10px 14px 14px}
676
+ .gkb-sk-editor-area{min-height:240px;max-height:40vh}
627
677
  }
628
678
  @media (prefers-color-scheme:dark){
629
679
  .gkb-sk-panel{box-shadow:0 48px 140px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.05)}
@@ -688,25 +738,21 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
688
738
  h("div", { key: "titles" }, [
689
739
  h("div", { key: "label", style: LABEL_STYLE }, "Навыки"),
690
740
  h("div", { key: "sub", style: SKILLS_SUB_STYLE }, totals
691
- ? pluralRuns(totals.runs) + " · " + totals.used + " из " + totals.skills + " использованы"
741
+ ? pluralRuns(totals.runs) + " · " + totals.used + "/" + totals.skills + " · ≈" + compactTokens(totals.tokens)
692
742
  : "загрузка статистики…")
693
743
  ]),
694
744
  h("span", { key: "count", style: SKILLS_COUNT_STYLE }, String(count))
695
745
  ]),
696
746
  active.length > 0
697
- ? h("div", { key: "active", style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "6px", minWidth: 0 } }, [
747
+ ? h("div", { key: "active", className: "gkb-skills-active", style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "6px", minWidth: 0 } }, [
698
748
  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)" } }),
699
749
  h("span", { key: "label", style: { fontSize: "9px", fontWeight: 750, letterSpacing: ".12em", textTransform: "uppercase", color: "var(--dsw-alias-brand-primary, #0E6E5E)", flex: "none" } }, "в работе"),
700
750
  h("span", { key: "names", style: { fontSize: "10px", color: "var(--dsw-alias-label-secondary, #666)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, activeNames.join(", ") + activeMore)
701
751
  ])
702
752
  : null,
703
- h("div", { key: "chips", style: SKILLS_CHIPS_STYLE }, categories.map((category) =>
753
+ h("div", { key: "chips", className: "gkb-skills-chips", style: SKILLS_CHIPS_STYLE }, categories.slice(0, 4).map((category) =>
704
754
  h("span", { key: category.id, style: SKILLS_CHIP_STYLE }, category.label + " " + (counts[category.id] || ""))
705
- )),
706
- 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)" } }, [
707
- h("span", { key: "text" }, "Открыть каталог"),
708
- h("span", { key: "arrow", style: { opacity: .8 } }, "→")
709
- ])
755
+ ))
710
756
  ]);
711
757
  }
712
758
 
@@ -720,6 +766,81 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
720
766
  const [detailLoading, setDetailLoading] = react.useState(false);
721
767
  const [detailError, setDetailError] = react.useState("");
722
768
  const [copied, setCopied] = react.useState(false);
769
+ const [editor, setEditor] = react.useState(null);
770
+ const [createDraft, setCreateDraft] = react.useState(null);
771
+ const [deleteConfirm, setDeleteConfirm] = react.useState(null);
772
+ const [notice, setNotice] = react.useState("");
773
+ const editorRef = react.useRef(null);
774
+ react.useEffect(() => {
775
+ if (editor && editorRef.current) editorRef.current.focus();
776
+ }, [editor && editor.name]);
777
+
778
+ const tokenEstimate = (text) => {
779
+ try { return Math.max(40, Math.round(new TextEncoder().encode(String(text)).length / 3.2)); } catch { return 0; }
780
+ };
781
+ const shortTokens = (value) => {
782
+ const n = Number(value) || 0;
783
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "").replace(".", ",") + "k";
784
+ return String(n);
785
+ };
786
+ const flash = (message) => {
787
+ setNotice(message);
788
+ setTimeout(() => setNotice(""), 3200);
789
+ };
790
+ const reloadSkills = async () => {
791
+ await loadSkills(store.sessionId, true);
792
+ };
793
+ const openEditor = () => {
794
+ if (!detail || !detail.skill) return;
795
+ setEditor({ name: detail.skill.name, content: detail.skill.content, saving: false, error: "", baseTokens: detail.skill.tokens });
796
+ };
797
+ const saveEditor = async () => {
798
+ if (!editor || editor.saving) return;
799
+ setEditor(Object.assign({}, editor, { saving: true, error: "" }));
800
+ try {
801
+ const saved = await callSkills("skill-save", { name: editor.name, content: editor.content, sessionId: store.sessionId || undefined });
802
+ setDetail((previous) => previous ? Object.assign({}, previous, { skill: saved }) : previous);
803
+ setEditor(null);
804
+ await reloadSkills();
805
+ flash("Сохранено · резервная копия в .dsh-kit-backups");
806
+ } catch (error) {
807
+ setEditor((previous) => previous ? Object.assign({}, previous, { saving: false, error: String(error && error.message ? error.message : error) }) : previous);
808
+ }
809
+ };
810
+ const submitCreate = async () => {
811
+ if (!createDraft || createDraft.busy) return;
812
+ const name = createDraft.name.trim();
813
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
814
+ setCreateDraft(Object.assign({}, createDraft, { error: "Имя: kebab-case из латиницы и цифр, например my-skill" }));
815
+ return;
816
+ }
817
+ setCreateDraft(Object.assign({}, createDraft, { busy: true, error: "" }));
818
+ try {
819
+ const result = createDraft.mode === "duplicate"
820
+ ? await callSkills("skill-duplicate", { source: createDraft.source, name, sessionId: store.sessionId || undefined })
821
+ : await callSkills("skill-create", { name, description: createDraft.description.trim(), sessionId: store.sessionId || undefined });
822
+ setCreateDraft(null);
823
+ await reloadSkills();
824
+ setSelected(result.skill.name);
825
+ flash(createDraft.mode === "duplicate" ? "Копия создана" : "Навык создан");
826
+ } catch (error) {
827
+ setCreateDraft((previous) => previous ? Object.assign({}, previous, { busy: false, error: String(error && error.message ? error.message : error) }) : previous);
828
+ }
829
+ };
830
+ const submitDelete = async () => {
831
+ if (!deleteConfirm || deleteConfirm.busy) return;
832
+ setDeleteConfirm(Object.assign({}, deleteConfirm, { busy: true, error: "" }));
833
+ try {
834
+ await callSkills("skill-delete", { name: deleteConfirm.name, sessionId: store.sessionId || undefined });
835
+ setDeleteConfirm(null);
836
+ setSelected(null);
837
+ setDetail(null);
838
+ await reloadSkills();
839
+ flash("Навык перемещён в .dsh-kit-trash — данные не потеряны");
840
+ } catch (error) {
841
+ setDeleteConfirm((previous) => previous ? Object.assign({}, previous, { busy: false, error: String(error && error.message ? error.message : error) }) : previous);
842
+ }
843
+ };
723
844
 
724
845
  react.useEffect(() => {
725
846
  if (store.open) void loadSkills(store.sessionId, true);
@@ -732,14 +853,22 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
732
853
  react.useEffect(() => {
733
854
  if (!store.open) return undefined;
734
855
  const onKey = (event) => {
856
+ if ((event.metaKey || event.ctrlKey) && String(event.key).toLowerCase() === "s" && editor) {
857
+ event.preventDefault();
858
+ void saveEditor();
859
+ return;
860
+ }
735
861
  if (event.key !== "Escape") return;
736
862
  event.stopPropagation();
863
+ if (editor) { setEditor(null); return; }
864
+ if (deleteConfirm) { setDeleteConfirm(null); return; }
865
+ if (createDraft) { setCreateDraft(null); return; }
737
866
  if (selected) { setSelected(null); setDetail(null); }
738
867
  else skillsStore.set({ open: false });
739
868
  };
740
869
  document.addEventListener("keydown", onKey, true);
741
870
  return () => document.removeEventListener("keydown", onKey, true);
742
- }, [store.open, selected]);
871
+ }, [store.open, selected, editor, deleteConfirm, createDraft]);
743
872
  react.useEffect(() => {
744
873
  if (!selected) { setDetail(null); setDetailError(""); return; }
745
874
  let alive = true;
@@ -804,7 +933,8 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
804
933
  h("span", { className: "gkb-sk-card-foot", key: "foot" }, [
805
934
  h("span", { className: "gkb-sk-meta", key: "meta" }, [
806
935
  h("span", { key: "files" }, h("b", null, "обновлён "), shortDate(skill.updatedAt)),
807
- h("span", { key: "used" }, h("b", null, skill.usage.count > 0 ? "последний раз " : "ещё не использовался"), skill.usage.count > 0 ? shortDate(skill.usage.lastAt) : "")
936
+ h("span", { key: "used" }, h("b", null, skill.usage.count > 0 ? "последний раз " : "ещё не использовался"), skill.usage.count > 0 ? shortDate(skill.usage.lastAt) : ""),
937
+ h("span", { key: "cost", className: "gkb-sk-cost", "data-heavy": skill.heavy ? "true" : "false" }, "ядро ≈" + shortTokens(skill.tokens))
808
938
  ]),
809
939
  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))
810
940
  ]),
@@ -830,6 +960,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
830
960
  h("div", { className: "gkb-sk-stat", key: "week" }, [h("span", { key: "l" }, "за неделю"), h("b", { key: "v" }, String(detail.usage ? detail.usage.week : 0))]),
831
961
  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" }) : "")]),
832
962
  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)]),
963
+ 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" : "в бюджете")]),
833
964
  h("div", { className: "gkb-sk-stat", key: "updated" }, [h("span", { key: "l" }, "обновлён"), h("b", { key: "v" }, shortDate(detail.skill.updatedAt))])
834
965
  ])
835
966
  : null,
@@ -837,11 +968,46 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
837
968
  ? h("div", { key: "when", className: "gkb-sk-when" }, [h("b", { key: "b" }, "Когда применять: "), detail.skill.whenToUse])
838
969
  : null,
839
970
  detail && detail.skill
840
- ? h("pre", { key: "content", className: "gkb-sk-content" }, detail.skill.content)
971
+ ? (editor
972
+ ? h("div", { key: "editor", className: "gkb-sk-editor" }, [
973
+ h("div", { key: "head", className: "gkb-sk-editor-head" }, [
974
+ h("span", { key: "t" }, "Редактирование SKILL.md"),
975
+ h("span", { key: "c", className: "gkb-sk-editor-cost", "data-heavy": tokenEstimate(editor.content) > 6000 ? "true" : "false" }, "≈" + shortTokens(tokenEstimate(editor.content)) + " токенов")
976
+ ]),
977
+ editor.error ? h("div", { key: "err", className: "gkb-sk-error" }, editor.error) : null,
978
+ h("textarea", {
979
+ key: "area",
980
+ ref: editorRef,
981
+ className: "gkb-sk-editor-area",
982
+ value: editor.content,
983
+ spellCheck: false,
984
+ "aria-label": "Содержимое SKILL.md",
985
+ onChange: (event) => setEditor(Object.assign({}, editor, { content: event.target.value })),
986
+ onKeyDown: (event) => {
987
+ if (event.key !== "Tab") return;
988
+ event.preventDefault();
989
+ const element = event.target;
990
+ const start = element.selectionStart;
991
+ const end = element.selectionEnd;
992
+ const next = editor.content.slice(0, start) + " " + editor.content.slice(end);
993
+ setEditor(Object.assign({}, editor, { content: next }));
994
+ requestAnimationFrame(() => { element.selectionStart = start + 2; element.selectionEnd = start + 2; });
995
+ }
996
+ }),
997
+ h("div", { key: "hint", className: "gkb-sk-editor-hint" }, "Ctrl+S — сохранить. Перед записью создаётся резервная копия в ~/.dsh/skills/.dsh-kit-backups. Держите ядро ≤ 6k токенов: детали — в references.")
998
+ ])
999
+ : h("pre", { key: "content", className: "gkb-sk-content" }, detail.skill.content))
841
1000
  : null
842
1001
  ]),
843
- h("div", { className: "gkb-sk-detail-foot" }, [
844
- h("button", { key: "open", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: () => { void callSkills("open-skill", { name: selected }).catch(() => {}); } }, "Открыть папку"),
1002
+ h("div", { className: "gkb-sk-detail-foot" }, editor ? [
1003
+ h("button", { key: "save", type: "button", className: "gkb-sk-btn", "data-tone": "primary", disabled: editor.saving, onClick: () => { void saveEditor(); } }, editor.saving ? "Сохраняю…" : "Сохранить"),
1004
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setEditor(null) }, "Отмена"),
1005
+ h("span", { key: "note", className: "gkb-sk-foot-note" }, "≈" + shortTokens(tokenEstimate(editor.content)) + " токенов · Ctrl+S")
1006
+ ] : [
1007
+ h("button", { key: "edit", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: openEditor }, "Редактировать"),
1008
+ h("button", { key: "dup", type: "button", className: "gkb-sk-btn", onClick: () => setCreateDraft({ mode: "duplicate", name: selected + "-copy", source: selected, description: "", error: "" }) }, "Дублировать"),
1009
+ h("button", { key: "del", type: "button", className: "gkb-sk-btn", "data-tone": "danger", onClick: () => setDeleteConfirm({ name: selected, error: "" }) }, "Удалить"),
1010
+ h("button", { key: "open", type: "button", className: "gkb-sk-btn", onClick: () => { void callSkills("open-skill", { name: selected }).catch(() => {}); } }, "Открыть папку"),
845
1011
  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 ? "Скопировано" : "Скопировать имя"),
846
1012
  h("button", { key: "close", type: "button", className: "gkb-sk-btn", onClick: () => { setSelected(null); setDetail(null); } }, "Закрыть"),
847
1013
  h("span", { key: "path", className: "gkb-sk-foot-note" }, detail && detail.skill ? detail.skill.path : "")
@@ -850,6 +1016,47 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
850
1016
  )
851
1017
  : null;
852
1018
 
1019
+ const createModal = createDraft
1020
+ ? h("div", { className: "gkb-sk-modal-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) setCreateDraft(null); } },
1021
+ h("div", { className: "gkb-sk-modal", role: "dialog", "aria-modal": "true", "aria-label": createDraft.mode === "duplicate" ? "Дублировать навык" : "Новый навык" }, [
1022
+ h("h3", { key: "title", className: "gkb-sk-modal-title" }, createDraft.mode === "duplicate" ? "Дублировать навык" : "Новый навык"),
1023
+ createDraft.mode === "duplicate"
1024
+ ? h("p", { key: "text", className: "gkb-sk-modal-text" }, "Копия навыка «" + createDraft.source + "» со всеми references.")
1025
+ : h("p", { key: "text", className: "gkb-sk-modal-text" }, "Появится папка в ~/.dsh/skills с компактным шаблоном: ядро ≤ 6k токенов, детали — в references."),
1026
+ h("label", { key: "name", className: "gkb-sk-field" }, [
1027
+ h("span", { key: "s" }, "Имя (kebab-case)"),
1028
+ 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 })) })
1029
+ ]),
1030
+ createDraft.mode === "create"
1031
+ ? h("label", { key: "desc", className: "gkb-sk-field" }, [
1032
+ h("span", { key: "s" }, "Короткое описание-триггер"),
1033
+ h("input", { key: "i", className: "gkb-sk-input", value: createDraft.description, placeholder: "Когда применять навык — одной строкой", onChange: (event) => setCreateDraft(Object.assign({}, createDraft, { description: event.target.value })) })
1034
+ ])
1035
+ : null,
1036
+ createDraft.error ? h("div", { key: "error", className: "gkb-sk-error" }, createDraft.error) : null,
1037
+ h("div", { key: "foot", className: "gkb-sk-modal-foot" }, [
1038
+ h("button", { key: "ok", type: "button", className: "gkb-sk-btn", "data-tone": "primary", disabled: createDraft.busy, onClick: () => { void submitCreate(); } }, createDraft.busy ? "Создаю…" : (createDraft.mode === "duplicate" ? "Создать копию" : "Создать навык")),
1039
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setCreateDraft(null) }, "Отмена")
1040
+ ])
1041
+ ])
1042
+ )
1043
+ : null;
1044
+
1045
+ const deleteModal = deleteConfirm
1046
+ ? h("div", { className: "gkb-sk-modal-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) setDeleteConfirm(null); } },
1047
+ h("div", { className: "gkb-sk-modal", role: "dialog", "aria-modal": "true", "aria-label": "Удалить навык" }, [
1048
+ h("h3", { key: "title", className: "gkb-sk-modal-title" }, "Удалить навык?"),
1049
+ h("p", { key: "text", className: "gkb-sk-modal-text" }, "Папка переместится в ~/.dsh/skills/.dsh-kit-trash — это обратимо, файлы не удаляются."),
1050
+ h("code", { key: "name", className: "gkb-sk-modal-code" }, deleteConfirm.name),
1051
+ deleteConfirm.error ? h("div", { key: "error", className: "gkb-sk-error" }, deleteConfirm.error) : null,
1052
+ h("div", { key: "foot", className: "gkb-sk-modal-foot" }, [
1053
+ h("button", { key: "ok", type: "button", className: "gkb-sk-btn", "data-tone": "danger", disabled: deleteConfirm.busy, onClick: () => { void submitDelete(); } }, deleteConfirm.busy ? "Переношу…" : "В корзину"),
1054
+ h("button", { key: "cancel", type: "button", className: "gkb-sk-btn", onClick: () => setDeleteConfirm(null) }, "Отмена")
1055
+ ])
1056
+ ])
1057
+ )
1058
+ : null;
1059
+
853
1060
  return h("div", { className: "gkb-sk-backdrop", onMouseDown: (event) => { if (event.target === event.currentTarget) close(); } },
854
1061
  h("div", { className: "gkb-sk-panel", role: "dialog", "aria-modal": "true", "aria-label": "Каталог навыков" },
855
1062
  h("div", { className: "gkb-sk-head" }, [
@@ -858,10 +1065,14 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
858
1065
  h("div", { key: "kicker", className: "gkb-sk-kicker" }, "DSH · навыки"),
859
1066
  h("h2", { key: "title", className: "gkb-sk-title" }, "Мои навыки"),
860
1067
  h("div", { key: "sub", className: "gkb-sk-sub" }, total
861
- ? [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 + " в работе" : ""]
1068
+ ? [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 + " в работе" : ""]
862
1069
  : "Загружаю статистику…")
863
1070
  ]),
864
- h("button", { key: "close", type: "button", className: "gkb-sk-close", onClick: close, "aria-label": "Закрыть каталог навыков" }, "×")
1071
+ h("div", { key: "actions", className: "gkb-sk-head-actions" }, [
1072
+ 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 {} } }, "Токены"),
1073
+ h("button", { key: "new", type: "button", className: "gkb-sk-btn", "data-tone": "primary", onClick: () => setCreateDraft({ mode: "create", name: "", description: "", error: "" }) }, "Новый навык"),
1074
+ h("button", { key: "close", type: "button", className: "gkb-sk-close", onClick: close, "aria-label": "Закрыть каталог навыков" }, "×")
1075
+ ])
865
1076
  ]),
866
1077
  h("div", { key: "search", className: "gkb-sk-search-row" }, [
867
1078
  h("input", { key: "input", className: "gkb-sk-search", placeholder: "Поиск по названию, описанию или категории…", value: query, onChange: (event) => setQuery(event.target.value), autoFocus: true, "aria-label": "Поиск навыков" }),
@@ -873,6 +1084,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
873
1084
  ])
874
1085
  ]),
875
1086
  activeBand,
1087
+ notice ? h("div", { key: "notice", className: "gkb-sk-notice", role: "status" }, notice) : null,
876
1088
  h("div", { key: "filters", className: "gkb-sk-filters" }, [
877
1089
  h("button", { key: "all", type: "button", className: "gkb-sk-chip", "data-active": category === "all" ? "true" : "false", onClick: () => setCategory("all") }, "Все " + skills.length),
878
1090
  ...categories.filter((item) => counts[item.id] > 0).map((item) => h("button", {
@@ -897,10 +1109,36 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
897
1109
  : null
898
1110
  ]),
899
1111
  detailPanel
900
- )
1112
+ ),
1113
+ createModal,
1114
+ deleteModal
901
1115
  );
902
1116
  }
903
1117
 
1118
+ /* Mobile rail cannot hold the tile stack; one pill opens the catalog. */
1119
+ function MobileSkillsFab() {
1120
+ const store = useSkillsStore();
1121
+ if (store.open) return null;
1122
+ const totals = store.data ? store.data.totals : null;
1123
+ const open = () => {
1124
+ skillsStore.set({ open: true, sessionId: store.sessionId || null });
1125
+ void loadSkills(store.sessionId, true);
1126
+ };
1127
+ return h("button", {
1128
+ type: "button",
1129
+ className: "gkb-fab",
1130
+ onClick: open,
1131
+ "aria-label": "Открыть каталог навыков"
1132
+ }, [
1133
+ h("svg", { key: "icon", width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" }, [
1134
+ 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" }),
1135
+ h("circle", { key: "b", cx: 19, cy: 5, r: 1.4, fill: "currentColor" })
1136
+ ]),
1137
+ h("span", { key: "label" }, "Навыки"),
1138
+ totals ? h("b", { key: "count" }, String(totals.skills)) : null
1139
+ ]);
1140
+ }
1141
+
904
1142
  function SpendRow(props) {
905
1143
  if (props && props.wide === false) return null;
906
1144
  const snap = useSnapshot();
@@ -926,13 +1164,16 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
926
1164
  h("span", { key: "value", style: amountStyle }, value)
927
1165
  ]);
928
1166
  }
929
- /* One slot registration: two stacked full-width AURORA tiles */
1167
+ /* One slot registration: AURORA tiles in a single full-width column.
1168
+ * Token Lab (a sibling client part) hands its tile over through a global;
1169
+ * rendering it here keeps the footer stack instead of a second slot row. */
930
1170
  function SidebarTiles(props) {
1171
+ const TokenTile = typeof window !== "undefined" ? window.__DSH_TOKEN_TILE__ : null;
931
1172
  return h("div", {
932
1173
  style: {
933
1174
  display: "flex",
934
1175
  flexDirection: "column",
935
- gap: "2px",
1176
+ gap: "4px",
936
1177
  width: "100%",
937
1178
  boxSizing: "border-box",
938
1179
  padding: "4px 8px 6px"
@@ -940,6 +1181,7 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
940
1181
  "data-gkb-tiles": "1"
941
1182
  }, [
942
1183
  h(SkillsRow, Object.assign({ key: "skills" }, props)),
1184
+ TokenTile ? h(TokenTile, { key: "tokens", useSessions: props && props.useSessions }) : null,
943
1185
  h(BalanceRow, { key: "balance" }),
944
1186
  h(SpendRow, { key: "spend" })
945
1187
  ]);
@@ -1092,6 +1334,516 @@ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1092
1334
  order: 640
1093
1335
  }, SkillsCatalog)), "gkb-ru: skills catalog overlay");
1094
1336
 
1337
+ ctx.effect(() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1338
+ name: "shell.overlay",
1339
+ id: "gkb-mobile-fab",
1340
+ order: 660
1341
+ }, MobileSkillsFab)), "gkb-ru: mobile skills fab");
1342
+
1343
+ }
1344
+
1345
+ exports.apply = apply;
1346
+ exports.inject = inject;
1347
+
1348
+ return module.exports;
1349
+ })());
1350
+
1351
+ /* ---- dsh-token-lab ---- */
1352
+ __parts.push((function () {
1353
+ var module = { exports: {} };
1354
+ var exports = module.exports;
1355
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
1356
+ const react = require("react");
1357
+ const h = react.createElement;
1358
+
1359
+ const inject = ["slots", "connection"];
1360
+
1361
+ let pluginCtx;
1362
+
1363
+ /* ------------------------------------------------------------------ */
1364
+ /* session selection + store */
1365
+ /* ------------------------------------------------------------------ */
1366
+
1367
+ function currentSessionSelector(state) {
1368
+ try {
1369
+ const sessions = Object.values(state.byId || {});
1370
+ const main = sessions.find((session) => session && session.retainedBy && (session.retainedBy.mainView || 0) > 0);
1371
+ return main ? main.id : undefined;
1372
+ } catch { return undefined; }
1373
+ }
1374
+
1375
+ const store = {
1376
+ state: { open: false, sessionId: null, report: null, loading: false, error: null, loadedAt: 0 },
1377
+ listeners: new Set(),
1378
+ get() { return this.state; },
1379
+ subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; },
1380
+ set(patch) {
1381
+ this.state = Object.assign({}, this.state, patch);
1382
+ for (const listener of Array.from(this.listeners)) { try { listener(); } catch {} }
1383
+ }
1384
+ };
1385
+ function useStore() {
1386
+ return react.useSyncExternalStore(store.subscribe.bind(store), store.get.bind(store), store.get.bind(store));
1387
+ }
1388
+
1389
+ let inflight = null;
1390
+ async function callToken(operation, payload) {
1391
+ if (!pluginCtx || !pluginCtx.connection) throw new Error("RPC недоступен");
1392
+ const result = await pluginCtx.connection.rpc.call("/api", "dsh-token-lab/" + operation, payload || {}, AbortSignal.timeout(20000));
1393
+ if (!result || result.ok !== true) throw new Error((result && result.error && result.error.message) || "ошибка RPC");
1394
+ return result.value;
1395
+ }
1396
+ function loadReport(sessionId, force) {
1397
+ const state = store.get();
1398
+ const wanted = sessionId || state.sessionId || null;
1399
+ if (!force && state.report && state.sessionId === wanted && Date.now() - state.loadedAt < 8000) return Promise.resolve(state.report);
1400
+ if (inflight) return inflight;
1401
+ store.set({ loading: true, error: null, sessionId: wanted });
1402
+ inflight = callToken("report", wanted ? { sessionId: wanted } : {})
1403
+ .then((report) => {
1404
+ store.set({ report, loading: false, error: null, loadedAt: Date.now() });
1405
+ return report;
1406
+ })
1407
+ .catch((error) => {
1408
+ store.set({ loading: false, error: String(error && error.message ? error.message : error) });
1409
+ return null;
1410
+ })
1411
+ .finally(() => { inflight = null; });
1412
+ return inflight;
1413
+ }
1414
+
1415
+ /* Cross-plugin opener: the skills catalog offers a "Токены" button that
1416
+ * works on mobile, where the sidebar tile rail is collapsed. */
1417
+ try {
1418
+ if (typeof window !== "undefined") {
1419
+ window.__DSH_TOKEN_OPEN__ = (sessionId) => {
1420
+ const wanted = sessionId || store.state.sessionId || null;
1421
+ store.set({ open: true, sessionId: wanted });
1422
+ void loadReport(wanted, true);
1423
+ };
1424
+ }
1425
+ } catch {}
1426
+
1427
+ /* ------------------------------------------------------------------ */
1428
+ /* formatting */
1429
+ /* ------------------------------------------------------------------ */
1430
+
1431
+ function num(value) {
1432
+ const n = Number(value) || 0;
1433
+ return n.toLocaleString("ru-RU");
1434
+ }
1435
+ function compact(value) {
1436
+ const n = Number(value) || 0;
1437
+ if (n >= 1000000) return (n / 1000000).toFixed(1).replace(".0", "").replace(".", ",") + "M";
1438
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "").replace(".", ",") + "k";
1439
+ return String(Math.round(n));
1440
+ }
1441
+ function percent(value, digits) {
1442
+ const n = Number(value) || 0;
1443
+ return (n * 100).toFixed(digits === undefined ? 0 : digits).replace(".", ",") + "%";
1444
+ }
1445
+ function pressureColor(pressure) {
1446
+ const p = Number(pressure) || 0;
1447
+ if (p >= 0.75) return "#8E2B18";
1448
+ if (p >= 0.5) return "#B07A2A";
1449
+ return "#0E6E5E";
1450
+ }
1451
+ function pressureLabel(pressure) {
1452
+ const p = Number(pressure) || 0;
1453
+ if (p >= 0.85) return "Почти переполнение — пора сжимать";
1454
+ if (p >= 0.65) return "Заполнено больше половины";
1455
+ if (p >= 0.35) return "Рабочий диапазон";
1456
+ return "Много свободного места";
1457
+ }
1458
+ function plural(count, one, few, many) {
1459
+ const n = Math.abs(Number(count) || 0) % 100;
1460
+ const last = n % 10;
1461
+ if (n > 10 && n < 20) return many;
1462
+ if (last === 1) return one;
1463
+ if (last >= 2 && last <= 4) return few;
1464
+ return many;
1465
+ }
1466
+
1467
+ /* ------------------------------------------------------------------ */
1468
+ /* primitives */
1469
+ /* ------------------------------------------------------------------ */
1470
+
1471
+ function Ring(props) {
1472
+ const pressure = Math.max(0, Math.min(1, Number(props.pressure) || 0));
1473
+ const color = pressureColor(pressure);
1474
+ const radius = 52;
1475
+ const circumference = 2 * Math.PI * radius;
1476
+ const dash = circumference * pressure;
1477
+ return h("svg", {
1478
+ className: "tl-ring",
1479
+ width: 132, height: 132, viewBox: "0 0 132 132",
1480
+ role: "img",
1481
+ "aria-label": "Заполнение контекстного окна " + percent(pressure)
1482
+ }, [
1483
+ h("circle", { key: "track", cx: 66, cy: 66, r: radius, fill: "none", stroke: "color-mix(in oklab, currentColor 12%, transparent)", strokeWidth: 9 }),
1484
+ h("circle", {
1485
+ key: "value", cx: 66, cy: 66, r: radius, fill: "none", stroke: color, strokeWidth: 9,
1486
+ strokeLinecap: "round", strokeDasharray: dash + " " + circumference,
1487
+ transform: "rotate(-90 66 66)",
1488
+ style: { transition: "stroke-dasharray 600ms cubic-bezier(.16,1,.3,1), stroke 300ms ease" }
1489
+ }),
1490
+ h("text", { key: "pct", x: 66, y: 62, textAnchor: "middle", fontSize: 25, fontWeight: 720, fill: "currentColor", style: { fontVariantNumeric: "tabular-nums", letterSpacing: "-.02em" } }, percent(pressure)),
1491
+ h("text", { key: "cap", x: 66, y: 82, textAnchor: "middle", fontSize: 9.5, fontWeight: 650, letterSpacing: ".12em", fill: "currentColor", opacity: 0.55 }, "КОНТЕКСТ")
1492
+ ]);
1493
+ }
1494
+
1495
+ function Sparkline(props) {
1496
+ const values = Array.isArray(props.series) ? props.series.filter((value) => Number.isFinite(value)) : [];
1497
+ if (values.length < 2) return null;
1498
+ const width = 560;
1499
+ const height = 74;
1500
+ const max = Math.max(1, ...values);
1501
+ const step = width / (values.length - 1);
1502
+ const points = values.map((value, index) => [index * step, height - 4 - (value / max) * (height - 12)]);
1503
+ const line = points.map((point, index) => (index === 0 ? "M" : "L") + point[0].toFixed(1) + " " + point[1].toFixed(1)).join(" ");
1504
+ const area = line + " L" + width + " " + height + " L0 " + height + " Z";
1505
+ const id = "tl-spark-" + (props.id || "main");
1506
+ return h("svg", { className: "tl-spark", viewBox: "0 0 " + width + " " + height, preserveAspectRatio: "none", "aria-hidden": "true" }, [
1507
+ h("defs", { key: "defs" }, h("linearGradient", { id, x1: 0, y1: 0, x2: 0, y2: 1 }, [
1508
+ h("stop", { key: "a", offset: "0%", stopColor: "var(--tl-accent, #0E6E5E)", stopOpacity: 0.28 }),
1509
+ h("stop", { key: "b", offset: "100%", stopColor: "var(--tl-accent, #0E6E5E)", stopOpacity: 0 })
1510
+ ])),
1511
+ h("path", { key: "area", d: area, fill: "url(#" + id + ")" }),
1512
+ h("path", { key: "line", d: line, fill: "none", stroke: "var(--tl-accent, #0E6E5E)", strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })
1513
+ ]);
1514
+ }
1515
+
1516
+ function River(props) {
1517
+ const groups = Array.isArray(props.groups) ? props.groups.filter((group) => group.tokens > 0) : [];
1518
+ const total = groups.reduce((sum, group) => sum + group.tokens, 0) || 1;
1519
+ return h("div", { className: "tl-river", role: "img", "aria-label": "Состав контекста по типам сообщений" },
1520
+ groups.map((group) => h("span", {
1521
+ key: group.id,
1522
+ className: "tl-river-seg",
1523
+ style: { width: Math.max(2, (group.tokens / total) * 100) + "%", background: group.color },
1524
+ title: group.label + ": " + num(group.tokens)
1525
+ }))
1526
+ );
1527
+ }
1528
+
1529
+ /* ------------------------------------------------------------------ */
1530
+ /* tile */
1531
+ /* ------------------------------------------------------------------ */
1532
+
1533
+ function TokenTile(props) {
1534
+ const state = useStore();
1535
+ const useSessions = props && props.useSessions;
1536
+ const sessionId = useSessions ? useSessions(currentSessionSelector) : undefined;
1537
+ react.useEffect(() => {
1538
+ if (sessionId) void loadReport(sessionId);
1539
+ }, [sessionId]);
1540
+ react.useEffect(() => {
1541
+ const timer = setInterval(() => {
1542
+ if (!document.hidden) void loadReport(sessionId, true);
1543
+ }, 20000);
1544
+ return () => clearInterval(timer);
1545
+ }, [sessionId]);
1546
+
1547
+ const report = state.report && (!sessionId || state.report.sessionId === sessionId) ? state.report : null;
1548
+ const total = report ? report.totalTokens : 0;
1549
+ const pressure = report ? report.pressure : 0;
1550
+ const color = pressureColor(pressure);
1551
+ const delta = report && report.surfaceDeltaTokens > 0 ? report.surfaceDeltaTokens : 0;
1552
+
1553
+ const open = () => {
1554
+ store.set({ open: true, sessionId: sessionId || state.sessionId });
1555
+ void loadReport(sessionId || state.sessionId, true);
1556
+ };
1557
+ return h("button", {
1558
+ type: "button",
1559
+ className: "tl-tile",
1560
+ "aria-label": "Открыть токен-лабораторию: " + (total ? num(total) + " токенов в контексте" : "нет данных"),
1561
+ onClick: open
1562
+ }, [
1563
+ h("div", { className: "tl-tile-head", key: "head" }, [
1564
+ h("span", { className: "tl-tile-label", key: "label" }, "Токены"),
1565
+ h("span", { className: "tl-tile-pct", key: "pct", style: { color } }, report ? percent(pressure) : "—"),
1566
+ h("span", { className: "tl-tile-dot", key: "dot", style: { background: color } })
1567
+ ]),
1568
+ h("div", { className: "tl-tile-row", key: "row" }, [
1569
+ h("b", { key: "b" }, report ? compact(total) : "—"),
1570
+ h("span", { className: "tl-tile-cap", key: "cap" }, report ? "из " + compact(report.contextWindow) : "нет сессии"),
1571
+ delta > 0 ? h("span", { className: "tl-tile-delta", key: "d" }, "+" + compact(delta)) : null
1572
+ ]),
1573
+ h("div", { className: "tl-tile-bar", key: "bar" }, h("span", { style: { width: Math.max(2, Math.min(100, pressure * 100)) + "%", background: color } }))
1574
+ ]);
1575
+ }
1576
+
1577
+ /* The kit's sidebar tile stack owns the footer slot; hand it this tile so
1578
+ * both live in one column instead of competing for the slot row. */
1579
+ try {
1580
+ if (typeof window !== "undefined") window.__DSH_TOKEN_TILE__ = TokenTile;
1581
+ } catch {}
1582
+
1583
+ /* ------------------------------------------------------------------ */
1584
+ /* panel */
1585
+ /* ------------------------------------------------------------------ */
1586
+
1587
+ function TokenPanel() {
1588
+ const state = useStore();
1589
+ react.useEffect(() => {
1590
+ if (!state.open) return undefined;
1591
+ void loadReport(state.sessionId, true);
1592
+ const timer = setInterval(() => {
1593
+ if (!document.hidden) void loadReport(state.sessionId, true);
1594
+ }, 10000);
1595
+ const onKey = (event) => { if (event.key === "Escape") store.set({ open: false }); };
1596
+ document.addEventListener("keydown", onKey);
1597
+ return () => { clearInterval(timer); document.removeEventListener("keydown", onKey); };
1598
+ }, [state.open, state.sessionId]);
1599
+
1600
+ if (!state.open) return null;
1601
+ const report = state.report;
1602
+ const close = () => store.set({ open: false });
1603
+ const pressure = report ? report.pressure : 0;
1604
+ const color = pressureColor(pressure);
1605
+ const groups = report ? report.groups : [];
1606
+ const top = report ? report.top : [];
1607
+ const skills = report ? report.library : null;
1608
+ const savings = skills ? Math.max(0, skills.totalTokens - skills.typicalTokens) : 0;
1609
+
1610
+ return h("div", { className: "tl-backdrop", onClick: (event) => { if (event.target === event.currentTarget) close(); } },
1611
+ h("section", { className: "tl-panel", role: "dialog", "aria-modal": "true", "aria-label": "Токен-лаборатория", style: { "--tl-accent": color } }, [
1612
+ h("header", { className: "tl-head", key: "head" }, [
1613
+ h("div", { key: "titles" }, [
1614
+ h("div", { className: "tl-kicker", key: "k" }, "ТОКЕН-ЛАБОРАТОРИЯ"),
1615
+ h("h2", { className: "tl-title", key: "t" }, "Куда уходит контекст"),
1616
+ h("p", { className: "tl-sub", key: "s" }, report
1617
+ ? "Текущая сессия · " + num(report.nodes) + " " + plural(report.nodes, "узел", "узла", "узлов") + " · обновляется вживую"
1618
+ : "Подключаемся к сессии…")
1619
+ ]),
1620
+ h("button", { className: "tl-close", key: "c", type: "button", onClick: close, "aria-label": "Закрыть" }, "×")
1621
+ ]),
1622
+
1623
+ state.error ? h("div", { className: "tl-error", key: "err" }, state.error) : null,
1624
+
1625
+ report ? h("div", { className: "tl-hero", key: "hero" }, [
1626
+ h("div", { className: "tl-hero-main", key: "main" }, [
1627
+ h("div", { className: "tl-big", key: "big" }, [
1628
+ h("b", { key: "n" }, num(report.totalTokens)),
1629
+ h("span", { key: "u" }, "токенов в контексте")
1630
+ ]),
1631
+ h("div", { className: "tl-meta", key: "meta" }, [
1632
+ h("span", { key: "window" }, "окно " + num(report.contextWindow)),
1633
+ h("span", { key: "left" }, "свободно " + num(Math.max(0, report.contextWindow - report.totalTokens))),
1634
+ report.surfaceDeltaTokens > 0 ? h("span", { key: "d", className: "tl-up" }, "+" + num(report.surfaceDeltaTokens) + " за последний шаг") : null
1635
+ ]),
1636
+ h("div", { className: "tl-status", key: "st", style: { color } }, [
1637
+ h("span", { className: "tl-status-dot", key: "d", style: { background: color } }),
1638
+ h("span", { key: "t" }, pressureLabel(pressure))
1639
+ ])
1640
+ ]),
1641
+ h("div", { className: "tl-hero-ring", key: "ring" }, [
1642
+ h(Ring, { key: "r", pressure }),
1643
+ h("div", { className: "tl-hero-hint", key: "h" }, report.baseline && report.baseline.kind === "usage" ? "по факту провайдера" : "оценка DSH")
1644
+ ]),
1645
+ h("div", { className: "tl-spark-wrap", key: "spark" }, [
1646
+ h("div", { className: "tl-spark-head", key: "sh" }, [
1647
+ h("span", { key: "l" }, "Рост контекста"),
1648
+ h("span", { key: "v" }, compact(report.totalTokens))
1649
+ ]),
1650
+ h(Sparkline, { key: "s", series: report.series })
1651
+ ])
1652
+ ]) : null,
1653
+
1654
+ report ? h("div", { className: "tl-grid", key: "grid" }, [
1655
+ h("div", { className: "tl-card tl-card-wide", key: "composition" }, [
1656
+ h("div", { className: "tl-card-head", key: "h" }, [
1657
+ h("h3", { key: "t" }, "Состав"),
1658
+ h("span", { key: "s" }, "по типам сообщений")
1659
+ ]),
1660
+ h(River, { key: "river", groups }),
1661
+ h("div", { className: "tl-legend", key: "legend" }, groups.map((group) => h("div", { className: "tl-legend-row", key: group.id }, [
1662
+ h("span", { className: "tl-swatch", key: "sw", style: { background: group.color } }),
1663
+ h("span", { className: "tl-legend-label", key: "l" }, group.label),
1664
+ h("span", { className: "tl-legend-count", key: "c" }, group.count + " " + plural(group.count, "узел", "узла", "узлов")),
1665
+ h("span", { className: "tl-legend-tokens", key: "t" }, num(group.tokens)),
1666
+ h("span", { className: "tl-legend-pct", key: "p" }, report.totalTokens > 0 ? percent(group.tokens / report.totalTokens) : "0%")
1667
+ ])))
1668
+ ]),
1669
+ h("div", { className: "tl-card", key: "top" }, [
1670
+ h("div", { className: "tl-card-head", key: "h" }, [
1671
+ h("h3", { key: "t" }, "Самые тяжёлые"),
1672
+ h("span", { key: "s" }, "топ-6 узлов")
1673
+ ]),
1674
+ h("div", { className: "tl-top", key: "list" }, top.length > 0 ? top.map((node, index) => {
1675
+ const max = Math.max(1, ...top.map((item) => item.tokens));
1676
+ return h("div", { className: "tl-top-row", key: node.seq }, [
1677
+ h("span", { className: "tl-rank", key: "r" }, String(index + 1).padStart(2, "0")),
1678
+ h("div", { className: "tl-top-body", key: "b" }, [
1679
+ h("div", { className: "tl-top-text", key: "t" }, node.label || "Системный блок"),
1680
+ h("div", { className: "tl-top-bar", key: "bar" }, h("span", { style: { width: Math.max(4, (node.tokens / max) * 100) + "%" } }))
1681
+ ]),
1682
+ h("span", { className: "tl-top-tokens", key: "n" }, num(node.tokens))
1683
+ ]);
1684
+ }) : h("div", { className: "tl-empty", key: "empty" }, "Пока пусто: в этой сессии ещё нет сообщений. Откройте рабочую сессию — состав появится здесь."))
1685
+ ]),
1686
+ h("div", { className: "tl-card tl-savings", key: "savings" }, [
1687
+ h("div", { className: "tl-card-head", key: "h" }, [
1688
+ h("h3", { key: "t" }, "Экономия навыков"),
1689
+ h("span", { key: "s" }, "библиотека " + (skills ? skills.count : 0) + " шт.")
1690
+ ]),
1691
+ h("div", { className: "tl-save-num", key: "n" }, [
1692
+ h("b", { key: "b" }, "−" + compact(savings)),
1693
+ h("span", { key: "s" }, "токенов против загрузки всей библиотеки")
1694
+ ]),
1695
+ h("div", { className: "tl-save-compare", key: "cmp" }, [
1696
+ h("div", { key: "a" }, [h("span", { key: "l" }, "вся библиотека"), h("b", { key: "v" }, compact(skills ? skills.totalTokens : 0))]),
1697
+ h("div", { className: "tl-save-arrow", key: "ar" }, "→"),
1698
+ h("div", { key: "b" }, [h("span", { key: "l" }, "3 средних ядра"), h("b", { key: "v", style: { color } }, compact(skills ? skills.typicalTokens : 0))])
1699
+ ]),
1700
+ h("div", { className: "tl-save-list", key: "list" }, (skills ? skills.heaviest.slice(0, 4) : []).map((skill) => h("div", { className: "tl-save-row", key: skill.name }, [
1701
+ h("span", { key: "n" }, skill.title),
1702
+ h("span", { key: "t" }, compact(skill.tokens))
1703
+ ]))),
1704
+ h("div", { className: "tl-save-foot", key: "foot" }, report.autopilot.mode === "on"
1705
+ ? "Автопилот подключает нужные ядра сам — до " + report.autopilot.maxSkills + " навыков"
1706
+ : "Автопилот выключен: навыки грузятся вручную")
1707
+ ])
1708
+ ]) : h("div", { className: "tl-skeleton", key: "sk" }, "Считаем контекст…"),
1709
+
1710
+ h("footer", { className: "tl-foot", key: "foot" }, [
1711
+ h("span", { key: "l" }, report ? "Сессия " + report.sessionId.slice(0, 18) + "…" : ""),
1712
+ h("span", { key: "r" }, report ? "обновлено " + new Date(report.generatedAt).toLocaleTimeString("ru-RU") : "")
1713
+ ])
1714
+ ])
1715
+ );
1716
+ }
1717
+
1718
+ /* ------------------------------------------------------------------ */
1719
+ /* styles */
1720
+ /* ------------------------------------------------------------------ */
1721
+
1722
+ const CSS = `
1723
+ .tl-tile{display:flex;flex-direction:column;gap:5px;width:100%;box-sizing:border-box;margin:0;padding:8px 10px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.10));background:var(--dsw-alias-settings-card-fill, rgba(0,0,0,.035));color:var(--dsw-alias-label-secondary,#555);font:inherit;text-align:left;cursor:pointer;transition:background-color .16s ease,border-color .16s ease,transform .16s ease}
1724
+ .tl-tile:hover{background:color-mix(in oklab,var(--dsw-alias-label-primary,#111) 6%,transparent);border-color:color-mix(in oklab,var(--dsw-alias-label-primary,#111) 18%,transparent)}
1725
+ .tl-tile:active{transform:scale(.995)}
1726
+ .tl-tile:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#0E6E5E);outline-offset:2px}
1727
+ .tl-tile-head{display:flex;align-items:center;justify-content:space-between;gap:6px}
1728
+ .tl-tile-label{font-size:9.5px;font-weight:650;letter-spacing:.1em;text-transform:uppercase;opacity:.72;flex:1 1 auto}
1729
+ .tl-tile-pct{font-size:10px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.01em}
1730
+ .tl-tile-dot{width:6px;height:6px;border-radius:999px;flex:0 0 auto}
1731
+ .tl-tile-row{display:flex;align-items:baseline;gap:5px;color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums;line-height:1;min-width:0}
1732
+ .tl-tile-row b{font-size:17px;font-weight:720;letter-spacing:-.02em}
1733
+ .tl-tile-cap{font-size:9.5px;font-weight:550;opacity:.55;white-space:nowrap}
1734
+ .tl-tile-bar{height:3px;border-radius:999px;background:color-mix(in oklab,currentColor 12%,transparent);overflow:hidden}
1735
+ .tl-tile-bar span{display:block;height:100%;border-radius:999px;transition:width .5s cubic-bezier(.16,1,.3,1)}
1736
+ .tl-tile-delta{font-size:9.5px;font-weight:700;color:#8E2B18;margin-left:auto}
1737
+
1738
+ .tl-backdrop{position:fixed;inset:0;z-index:9050;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb,#070b0a 44%,transparent);animation:tl-fade .18s ease-out}
1739
+ .tl-panel{position:relative;display:flex;flex-direction:column;width:min(1080px,94vw);max-height:90vh;overflow:auto;overscroll-behavior:contain;border-radius:20px;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);box-shadow:0 48px 140px -50px rgba(0,10,8,.6),0 1px 0 color-mix(in srgb,#fff 65%,transparent) inset;animation:tl-in .32s cubic-bezier(.16,1,.3,1)}
1740
+ .tl-head{flex:none;display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:24px 26px 16px;background:radial-gradient(120% 160% at 100% 0%,color-mix(in srgb,var(--tl-accent,#0E6E5E) 10%,transparent) 0%,transparent 58%),linear-gradient(180deg,color-mix(in srgb,var(--dsw-alias-bg-layer-1,#f6f7f6) 70%,transparent),transparent);border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
1741
+ .tl-kicker{display:flex;align-items:center;gap:9px;font-size:10px;font-weight:750;letter-spacing:.18em;color:var(--dsw-alias-label-secondary,#6b7280)}
1742
+ .tl-kicker::before{content:"";width:26px;height:1px;background:color-mix(in srgb,var(--tl-accent,#0E6E5E) 65%,transparent)}
1743
+ .tl-title{margin:7px 0 0;font-size:25px;line-height:1.15;font-weight:720;letter-spacing:-.025em}
1744
+ .tl-sub{margin:5px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472);font-variant-numeric:tabular-nums}
1745
+ .tl-close{flex:none;width:34px;height:34px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));background:transparent;color:var(--dsw-alias-label-secondary,#555);font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
1746
+ .tl-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary,#111)}
1747
+ .tl-close:focus-visible,.tl-panel button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#0E6E5E);outline-offset:2px}
1748
+ .tl-hero{display:grid;grid-template-columns:minmax(0,1.25fr) auto minmax(0,1fr);gap:26px;align-items:center;padding:24px 26px 20px}
1749
+ .tl-big{display:flex;align-items:baseline;gap:10px;font-variant-numeric:tabular-nums}
1750
+ .tl-big b{font-size:44px;line-height:1;font-weight:730;letter-spacing:-.04em}
1751
+ .tl-big span{font-size:12px;font-weight:550;color:var(--dsw-alias-label-secondary,#5b6472)}
1752
+ .tl-meta{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px;font-size:11.5px;color:var(--dsw-alias-label-secondary,#5b6472);font-variant-numeric:tabular-nums}
1753
+ .tl-meta .tl-up{color:#8E2B18;font-weight:650}
1754
+ .tl-status{display:inline-flex;align-items:center;gap:8px;margin-top:14px;font-size:12px;font-weight:650}
1755
+ .tl-status-dot{width:8px;height:8px;border-radius:999px;box-shadow:0 0 0 4px color-mix(in srgb,currentColor 14%,transparent)}
1756
+ .tl-hero-ring{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--dsw-alias-label-primary,#111)}
1757
+ .tl-ring{display:block}
1758
+ .tl-hero-hint{font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#7b8490)}
1759
+ .tl-spark-wrap{display:flex;flex-direction:column;gap:8px;min-width:0}
1760
+ .tl-spark-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#7b8490);font-weight:650}
1761
+ .tl-spark-head span:last-child{font-size:12px;letter-spacing:0;text-transform:none;color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums}
1762
+ .tl-spark{width:100%;height:74px;display:block}
1763
+ .tl-grid{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);gap:14px;padding:4px 26px 22px}
1764
+ .tl-card{border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-radius:14px;padding:16px 16px 14px;background:var(--dsw-alias-bg-layer-1,#f7f8f7);min-width:0}
1765
+ .tl-card-wide{grid-column:1 / -1}
1766
+ .tl-card-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}
1767
+ .tl-card-head h3{margin:0;font-size:12px;font-weight:700;letter-spacing:.02em}
1768
+ .tl-card-head span{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#7b8490);font-weight:650}
1769
+ .tl-river{display:flex;height:12px;border-radius:999px;overflow:hidden;background:color-mix(in oklab,currentColor 8%,transparent)}
1770
+ .tl-river-seg{display:block;height:100%;min-width:3px;transition:width .55s cubic-bezier(.16,1,.3,1);box-shadow:inset -1px 0 0 color-mix(in srgb,#fff 30%,transparent)}
1771
+ .tl-legend{display:flex;flex-direction:column;margin-top:12px}
1772
+ .tl-legend-row{display:grid;grid-template-columns:10px minmax(0,1fr) 76px 64px 44px;align-items:center;gap:10px;padding:5px 0;border-top:1px solid color-mix(in oklab,currentColor 7%,transparent);font-size:11.5px;font-variant-numeric:tabular-nums}
1773
+ .tl-legend-row:first-child{border-top:0}
1774
+ .tl-swatch{width:8px;height:8px;border-radius:2px}
1775
+ .tl-legend-label{color:var(--dsw-alias-label-primary,#111);font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1776
+ .tl-legend-count{color:var(--dsw-alias-label-secondary,#7b8490);font-size:10.5px}
1777
+ .tl-legend-tokens{text-align:right;font-weight:650}
1778
+ .tl-legend-pct{text-align:right;color:var(--dsw-alias-label-secondary,#7b8490);font-size:10.5px}
1779
+ .tl-top{display:flex;flex-direction:column;gap:10px}
1780
+ .tl-top-row{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:10px;align-items:center}
1781
+ .tl-rank{font-size:10px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#7b8490)}
1782
+ .tl-top-body{min-width:0;display:flex;flex-direction:column;gap:5px}
1783
+ .tl-top-text{font-size:11.5px;line-height:15px;color:var(--dsw-alias-label-primary,#111);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1784
+ .tl-top-bar{height:3px;border-radius:999px;background:color-mix(in oklab,currentColor 9%,transparent);overflow:hidden}
1785
+ .tl-top-bar span{display:block;height:100%;border-radius:999px;background:var(--tl-accent,#0E6E5E);opacity:.85;transition:width .5s cubic-bezier(.16,1,.3,1)}
1786
+ .tl-top-tokens{font-size:11px;font-weight:650;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#5b6472)}
1787
+ .tl-savings{display:flex;flex-direction:column}
1788
+ .tl-save-num{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
1789
+ .tl-save-num b{font-size:30px;line-height:1;font-weight:730;letter-spacing:-.035em;color:#0E6E5E;font-variant-numeric:tabular-nums}
1790
+ .tl-save-num span{font-size:11px;color:var(--dsw-alias-label-secondary,#5b6472);max-width:130px;line-height:14px}
1791
+ .tl-save-compare{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:10px;border:1px dashed color-mix(in oklab,currentColor 16%,transparent);font-variant-numeric:tabular-nums}
1792
+ .tl-save-compare>div{display:flex;flex-direction:column;gap:2px}
1793
+ .tl-save-compare span{font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#7b8490)}
1794
+ .tl-save-compare b{font-size:16px;font-weight:700}
1795
+ .tl-save-arrow{font-size:15px;opacity:.5;display:flex;align-items:center}
1796
+ .tl-save-list{margin-top:10px;display:flex;flex-direction:column}
1797
+ .tl-save-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0;border-top:1px solid color-mix(in oklab,currentColor 7%,transparent);font-size:11px;color:var(--dsw-alias-label-secondary,#5b6472)}
1798
+ .tl-save-row span:last-child{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
1799
+ .tl-save-foot{margin-top:auto;padding-top:10px;font-size:10.5px;line-height:15px;color:var(--dsw-alias-label-secondary,#7b8490)}
1800
+ .tl-error{margin:0 26px 8px;padding:10px 12px;border-radius:10px;border:1px solid color-mix(in srgb,#8E2B18 35%,transparent);background:color-mix(in srgb,#8E2B18 8%,transparent);color:#8E2B18;font-size:12px}
1801
+ .tl-skeleton{padding:60px 26px;text-align:center;color:var(--dsw-alias-label-secondary,#7b8490);font-size:13px}
1802
+ .tl-empty{padding:18px 2px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#7b8490)}
1803
+ .tl-foot{flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 26px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07));font-size:10px;color:var(--dsw-alias-label-secondary,#7b8490);font-variant-numeric:tabular-nums}
1804
+ @keyframes tl-fade{from{opacity:0}to{opacity:1}}
1805
+ @keyframes tl-in{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}
1806
+ @media (max-width: 900px){
1807
+ .tl-hero{grid-template-columns:minmax(0,1fr) auto;gap:18px}
1808
+ .tl-spark-wrap{grid-column:1 / -1}
1809
+ .tl-grid{grid-template-columns:1fr}
1810
+ .tl-card-wide{grid-column:auto}
1811
+ }
1812
+ @media (max-width: 640px){
1813
+ .tl-backdrop{padding:0}
1814
+ .tl-panel{width:100%;max-height:100vh;height:100%;border-radius:0;border-left:0;border-right:0}
1815
+ .tl-head{padding:18px 16px 12px}
1816
+ .tl-title{font-size:21px}
1817
+ .tl-hero{grid-template-columns:1fr;padding:18px 16px 14px;gap:14px}
1818
+ .tl-hero-ring{flex-direction:row;justify-content:flex-start;gap:14px}
1819
+ .tl-big b{font-size:34px}
1820
+ .tl-grid{padding:4px 16px 18px}
1821
+ .tl-legend-row{grid-template-columns:8px minmax(0,1fr) auto}
1822
+ .tl-legend-count{display:none}
1823
+ .tl-legend-pct{display:none}
1824
+ .tl-foot{padding:10px 16px}
1825
+ }
1826
+ `;
1827
+
1828
+ /* ------------------------------------------------------------------ */
1829
+ /* apply */
1830
+ /* ------------------------------------------------------------------ */
1831
+
1832
+ function apply(ctx) {
1833
+ pluginCtx = ctx;
1834
+ ctx.effect(() => {
1835
+ const style = document.createElement("style");
1836
+ style.id = "dsh-token-lab";
1837
+ style.textContent = CSS;
1838
+ document.head.appendChild(style);
1839
+ return () => style.remove();
1840
+ }, "token-lab: styles");
1841
+
1842
+ ctx.effect(() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1843
+ name: "shell.overlay",
1844
+ id: "token-lab-panel",
1845
+ order: 650
1846
+ }, TokenPanel)), "token-lab: panel");
1095
1847
  }
1096
1848
 
1097
1849
  exports.apply = apply;