dsh-ws-recency 0.10.1 → 0.10.3

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 (3) hide show
  1. package/README.md +2 -2
  2. package/client.js +141 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,7 +65,7 @@ DSH 原生的置顶只把会话顶到**它所属工作区分组的第一行**,
65
65
 
66
66
  ## 六、Token 消耗统计
67
67
 
68
- 会话「…」菜单 →「Token 消耗统计」:
68
+ 会话下方统计条上的「用量分析」,或会话「…」菜单 →「Token 消耗统计」:
69
69
 
70
70
  - **真实账单**(每步记录的 usage):步数、输入(未缓存)、缓存命中、输出、合计;
71
71
  - **结构占比**(按内容估算):主 agent 自己(思考 / 回复 / 用户消息 / 系统提示)vs 每个工具,一条占比条 + 图例;
@@ -110,7 +110,7 @@ dsh plugin --profile <profile> add github:chyxhtc/dsh-ws-recency
110
110
  或固定版本的预构建包:
111
111
 
112
112
  ```powershell
113
- dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.10.1/dsh-ws-recency-0.10.1.tgz
113
+ dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.10.3/dsh-ws-recency-0.10.3.tgz
114
114
  ```
115
115
 
116
116
  装完把 `dsh-ws-recency` 加进 `~/.hypers-work/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,重启应用。
package/client.js CHANGED
@@ -85,6 +85,7 @@
85
85
  var pinHidden = new Set();
86
86
  var pinHost = null;
87
87
  var expandClicks = new Map();
88
+ var composerPill = null;
88
89
  var expandedSections = new Set();
89
90
  var headerMarked = new WeakSet();
90
91
  var disposed = false;
@@ -104,6 +105,8 @@
104
105
  ".wsr-pin-unpin{flex:none;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px;visibility:hidden}",
105
106
  ".wsr-pin-row:hover .wsr-pin-unpin{visibility:visible}",
106
107
  ".wsr-pin-unpin:hover{color:var(--dsw-alias-label-primary)}",
108
+ ".wsr-pill{box-sizing:border-box;border:none;background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-variant-numeric:tabular-nums;line-height:inherit;white-space:nowrap;border-radius:var(--dsw-radius-sm,6px);display:inline-flex;align-items:center;gap:6px;padding:1px 8px;cursor:pointer}",
109
+ ".wsr-pill:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));color:var(--dsw-alias-label-secondary,#cfd3d8)}",
107
110
  ".wsr-modal-backdrop{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45)}",
108
111
  ".wsr-modal{width:min(720px,92vw);max-height:84vh;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1,#1f2226);color:var(--dsw-alias-label-primary,#e8e8e8);border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.14));border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.45);overflow:hidden}",
109
112
  ".wsr-modal-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1))}",
@@ -181,6 +184,7 @@
181
184
  "stats.agentDetail": "主 agent 构成:思考 {reasoning} · 回复 {text} · 用户消息 {user} · 系统提示 {system}",
182
185
  "stats.diagnose.unmounted": "\n\n(宿主半已加载,但统计路由没挂上——重启应用再试)",
183
186
  "stats.diagnose.noHost": "\n\n(宿主半未加载:需要重启应用加载新版本)",
187
+ "composer.analysis": "用量分析",
184
188
  "trash.confirm": "\u5220\u9664\u8fd9\u4e2a\u4f1a\u8bdd\uff1f\n\n\u4f1a\u5148\u505c\u6b62\u5b83\u6b63\u5728\u8fdb\u884c\u7684\u5de5\u4f5c\uff0c\u518d\u628a\u65e5\u5fd7\u76ee\u5f55\u79fb\u5165\u56de\u6536\u7ad9\uff08\u4e0d\u662f\u7269\u7406\u5220\u9664\uff0c\u53ef\u4ee5\u624b\u52a8\u6062\u590d\uff09\u3002",
185
189
  "trash.failed": "\u5220\u9664\u5931\u8d25\uff1a",
186
190
  };
@@ -221,17 +225,32 @@
221
225
  "stats.agentDetail": "Agent composition: reasoning {reasoning} \u00b7 reply {text} \u00b7 user messages {user} \u00b7 system prompt {system}",
222
226
  "stats.diagnose.unmounted": "\n\n(the host half is loaded but the statistics route did not mount - restart the app and retry)",
223
227
  "stats.diagnose.noHost": "\n\n(the host half is not loaded: restart the app to pick up the new version)",
228
+ "composer.analysis": "Usage analysis",
224
229
  "trash.confirm": "Delete this session?\n\nIt stops whatever the session is running, then moves the log directory into the trash folder (a move, not an erase: you can restore it by hand).",
225
230
  "trash.failed": "Delete failed: ",
226
231
  };
227
232
 
228
- var FALLBACK = { "fold.show": "Show history ({n})", "fold.hide": "Hide history" };
233
+ /* Resolved lazily: the dictionaries are declared further down this closure. */
234
+ var resolvedDictionary = null;
235
+
236
+ function fallbackDictionary() {
237
+ if (resolvedDictionary !== null) return resolvedDictionary;
238
+ var language = "";
239
+ /* The page's own lang wins: it is what the UI is actually rendered in. */
240
+ try { if (typeof document !== "undefined" && document.documentElement !== null && document.documentElement !== undefined) language = document.documentElement.lang || ""; } catch (error) { language = ""; }
241
+ try { if (language === "" && typeof navigator !== "undefined" && navigator.language) language = navigator.language; } catch (error) { /* keep empty */ }
242
+ resolvedDictionary = String(language).toLowerCase().indexOf("zh") === 0 ? ZH : EN;
243
+ return resolvedDictionary;
244
+ }
229
245
 
230
246
  function t(key, params) {
231
247
  if (typeof data.t === "function") {
232
- try { return data.t(key, params); } catch (error) { /* fall back below */ }
248
+ var translated = null;
249
+ try { translated = data.t(key, params); } catch (error) { translated = null; }
250
+ if (typeof translated === "string" && translated !== key) return translated;
233
251
  }
234
- var text = FALLBACK[key] || key;
252
+ var dictionary = fallbackDictionary();
253
+ var text = (dictionary && dictionary[key]) || key;
235
254
  if (params !== undefined && params.n !== undefined) text = text.replace("{n}", String(params.n));
236
255
  return text;
237
256
  }
@@ -785,20 +804,122 @@
785
804
  }
786
805
  }
787
806
 
807
+ /** The session the conversation view is showing, from its own DOM hook. */
808
+ function currentConversationId() {
809
+ if (typeof document === "undefined") return null;
810
+ var element = document.querySelector("[data-conversation-session]");
811
+ if (element === null || typeof element.getAttribute !== "function") return null;
812
+ var id = element.getAttribute("data-conversation-session");
813
+ return typeof id === "string" && id !== "" ? id : null;
814
+ }
815
+
816
+ /** The nearest horizontal flex row above the stats: the whole footer dock. */
817
+ function composerDockOf(row) {
818
+ var node = row;
819
+ for (var depth = 0; depth < 4; depth += 1) {
820
+ var parent = node.parentElement;
821
+ if (parent === null) break;
822
+ var style = null;
823
+ try {
824
+ style = typeof window !== "undefined" && typeof window.getComputedStyle === "function"
825
+ ? window.getComputedStyle(parent)
826
+ : null;
827
+ } catch (error) {
828
+ style = null;
829
+ }
830
+ if (style !== null && style.display === "flex" && (style.flexDirection === "row" || style.flexDirection === "")) return parent;
831
+ node = parent;
832
+ }
833
+ return row.parentElement === null ? row : row.parentElement;
834
+ }
835
+
836
+ /** The host's own pill in that row, used as the visual template. */
837
+ function pillTemplateOf(row) {
838
+ if (typeof row.querySelector !== "function") return null;
839
+ var button = row.querySelector('button[class*="_pill"]');
840
+ return button !== null ? button : row.querySelector('[class*="_pill"]');
841
+ }
842
+
843
+ function cloneIconOf(template) {
844
+ if (template === null || typeof template.querySelector !== "function") return null;
845
+ var icon = template.querySelector("svg");
846
+ if (icon === null || typeof icon.cloneNode !== "function") return null;
847
+ try { return icon.cloneNode(true); } catch (error) { return null; }
848
+ }
849
+
850
+ /**
851
+ * Add one pill at the END of the composer footer that opens the analysis
852
+ * panel for the conversation on screen.
853
+ *
854
+ * It reuses the host pill's own class names and icon so it reads as part of
855
+ * the same row, sits after the context ring rather than between the stats,
856
+ * is re-attached whenever the host re-renders, and disappears with the row.
857
+ */
858
+ function decorateComposerStats() {
859
+ if (typeof document === "undefined") return;
860
+ var existing = document.querySelector("[data-wsr-composer-analysis]");
861
+ var seat = document.querySelector("[data-composer-seat]");
862
+ var anchors = seat === null || typeof seat.querySelectorAll !== "function" ? [] : seat.querySelectorAll("[data-composer-stat]");
863
+ if (anchors.length === 0 || currentConversationId() === null) {
864
+ if (existing !== null) existing.remove();
865
+ composerPill = null;
866
+ return;
867
+ }
868
+ var row = anchors[0].parentElement;
869
+ if (row === null) return;
870
+ var dock = composerDockOf(row);
871
+
872
+ if (existing === null) {
873
+ var template = pillTemplateOf(row);
874
+ var button = document.createElement("button");
875
+ button.type = "button";
876
+ button.setAttribute("data-wsr-composer-analysis", "true");
877
+ var base = template !== null && typeof template.className === "string" ? template.className : "";
878
+ button.className = base === "" ? "wsr-pill" : base + " wsr-pill";
879
+ var icon = cloneIconOf(template);
880
+ if (icon !== null) button.appendChild(icon);
881
+ var label = document.createElement("span");
882
+ var labelTemplate = template === null || typeof template.querySelector !== "function" ? null : template.querySelector('[class*="_label"]');
883
+ label.className = labelTemplate !== null && typeof labelTemplate.className === "string" && labelTemplate.className !== ""
884
+ ? labelTemplate.className
885
+ : "wsr-pill-label";
886
+ label.textContent = t("composer.analysis");
887
+ button.appendChild(label);
888
+ button.addEventListener("click", function () {
889
+ var id = currentConversationId();
890
+ if (id !== null) openStatsPanel(id);
891
+ });
892
+ composerPill = button;
893
+ } else {
894
+ composerPill = existing;
895
+ }
896
+ var children = dock.children || [];
897
+ if (composerPill.parentElement !== dock || children[children.length - 1] !== composerPill) {
898
+ dock.appendChild(composerPill);
899
+ }
900
+ }
901
+
788
902
  function apply() {
789
903
  if (disposed) return;
790
904
  var sessions = data.sessions;
791
905
  var workspaces = data.workspaces;
792
- if (!ready(sessions) || !ready(workspaces)) return;
793
-
794
906
  var settings = settingsOf();
907
+ if (!ready(sessions) || !ready(workspaces)) {
908
+ decorateComposerStats();
909
+ return;
910
+ }
911
+
795
912
  var entries = collect();
796
- if (entries.length === 0) return;
913
+ if (entries.length === 0) {
914
+ decorateComposerStats();
915
+ return;
916
+ }
797
917
 
798
918
  orderGroups(entries, latestByWorkspace(sessions, workspaces));
799
919
  foldRows(entries, settings, sessions, Date.now());
800
920
  renderPinned(entries, sessions, workspaces, settings);
801
921
  if (settings.colors) decorateRows(entries, sessions, data.statuses, settings);
922
+ decorateComposerStats();
802
923
  }
803
924
 
804
925
  function schedule() {
@@ -846,7 +967,7 @@
846
967
  return h(Item, {
847
968
  icon: typeof Icon === "function" ? h(Icon, {}) : undefined,
848
969
  onSelect: function () { props.actions.toggleFollowUp(props.sessionId); },
849
- }, props.t(flagged ? "menu.unflag" : "menu.flag"));
970
+ }, t(flagged ? "menu.unflag" : "menu.flag"));
850
971
  }
851
972
 
852
973
  /**
@@ -910,7 +1031,7 @@
910
1031
  if (typeof window.alert === "function") window.alert(t("trash.failed") + (error && error.message ? error.message : error));
911
1032
  });
912
1033
  },
913
- }, props.t("menu.trash"));
1034
+ }, t("menu.trash"));
914
1035
  }
915
1036
 
916
1037
  /* ------------------------- token statistics ------------------------- */
@@ -1118,7 +1239,7 @@
1118
1239
  return h(Item, {
1119
1240
  icon: typeof Icon === "function" ? h(Icon, {}) : undefined,
1120
1241
  onSelect: function () { openStatsPanel(props.sessionId); },
1121
- }, props.t("menu.stats"));
1242
+ }, t("menu.stats"));
1122
1243
  }
1123
1244
 
1124
1245
  /** Settings -> General row: how old a session must be before it is folded. */
@@ -1127,11 +1248,11 @@
1127
1248
  var days = Number(settings.foldDays);
1128
1249
  if (!Number.isFinite(days)) days = DEFAULT_DAYS;
1129
1250
  var keep = settings.keepActive !== false;
1130
- var text = props.t("settings.title");
1251
+ var text = t("settings.title");
1131
1252
  return h("div", { className: "wsr-setRow" },
1132
1253
  h("div", { className: "wsr-setText" },
1133
1254
  h("div", { className: "wsr-setTitle" }, text),
1134
- h("div", { className: "wsr-setDesc" }, props.t("settings.description")),
1255
+ h("div", { className: "wsr-setDesc" }, t("settings.description")),
1135
1256
  ),
1136
1257
  h("div", { className: "wsr-controls" },
1137
1258
  h("button", {
@@ -1161,7 +1282,7 @@
1161
1282
  checked: keep,
1162
1283
  onChange: function (event) { props.actions.setKeepActive(event.target.checked); },
1163
1284
  }),
1164
- props.t("settings.exempt")),
1285
+ t("settings.exempt")),
1165
1286
  ),
1166
1287
  );
1167
1288
  }
@@ -1170,11 +1291,11 @@
1170
1291
  function ColorsRow(props) {
1171
1292
  var settings = props.useStore(function (state) { return state; });
1172
1293
  var enabled = settings.colors !== false;
1173
- var text = props.t("color.title");
1294
+ var text = t("color.title");
1174
1295
  return h("div", { className: "wsr-setRow" },
1175
1296
  h("div", { className: "wsr-setText" },
1176
1297
  h("div", { className: "wsr-setTitle" }, text),
1177
- h("div", { className: "wsr-setDesc" }, props.t("color.description")),
1298
+ h("div", { className: "wsr-setDesc" }, t("color.description")),
1178
1299
  ),
1179
1300
  h("div", { className: "wsr-controls" },
1180
1301
  h("label", { className: "wsr-exempt" },
@@ -1183,7 +1304,7 @@
1183
1304
  checked: enabled,
1184
1305
  onChange: function (event) { props.actions.setColors(event.target.checked); },
1185
1306
  }),
1186
- props.t("color.enabled")),
1307
+ t("color.enabled")),
1187
1308
  ),
1188
1309
  );
1189
1310
  }
@@ -1192,11 +1313,11 @@
1192
1313
  function PinSectionRow(props) {
1193
1314
  var settings = props.useStore(function (state) { return state; });
1194
1315
  var enabled = settings.pinnedSection !== false;
1195
- var text = props.t("pin.title");
1316
+ var text = t("pin.title");
1196
1317
  return h("div", { className: "wsr-setRow" },
1197
1318
  h("div", { className: "wsr-setText" },
1198
1319
  h("div", { className: "wsr-setTitle" }, text),
1199
- h("div", { className: "wsr-setDesc" }, props.t("pin.description")),
1320
+ h("div", { className: "wsr-setDesc" }, t("pin.description")),
1200
1321
  ),
1201
1322
  h("div", { className: "wsr-controls" },
1202
1323
  h("label", { className: "wsr-exempt" },
@@ -1205,7 +1326,7 @@
1205
1326
  checked: enabled,
1206
1327
  onChange: function (event) { props.actions.setPinnedSection(event.target.checked); },
1207
1328
  }),
1208
- props.t("pin.enabled")),
1329
+ t("pin.enabled")),
1209
1330
  ),
1210
1331
  );
1211
1332
  }
@@ -1236,6 +1357,8 @@
1236
1357
  });
1237
1358
  pinHidden.clear();
1238
1359
  if (pinHost !== null && pinHost.parentElement !== null) pinHost.remove();
1360
+ if (composerPill !== null && typeof composerPill.remove === "function") composerPill.remove();
1361
+ composerPill = null;
1239
1362
  pinHost = null;
1240
1363
  expandClicks.clear();
1241
1364
  roots.forEach(function (root) { root.removeAttribute(ROOT_ATTR); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ws-recency",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, a global pinned section, follow-up marks, session delete into a recoverable trash, and per-session token statistics split by tool.",
5
5
  "license": "MIT",
6
6
  "author": "chyxhtc",