@morlay/ui-conversation-manager 0.0.2-alpha.3 → 0.0.2-alpha.5

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.
package/dist/client.cjs CHANGED
@@ -143,14 +143,20 @@ window.__ModuleLoader__.load({
143
143
  if (value < 1e9) return `${trim(value / 1e6)}M`;
144
144
  return `${trim(value / 1e9)}B`;
145
145
  }
146
+ /** @param value - 计数(轮次 / 步骤 / 用户输入 / 工具调用)。 @returns 千分位原文,例如 `1,024`。 */
147
+ function formatCount(value) {
148
+ if (!Number.isFinite(value) || value <= 0) return "0";
149
+ return Math.round(value).toLocaleString("en-US");
150
+ }
146
151
  /** @param value - 百分点(0~100)。 @returns 紧凑百分比,例如 `98.4%`、`100%`。 */
147
152
  function formatPercent(value) {
148
153
  if (!Number.isFinite(value) || value <= 0) return "0%";
149
154
  return `${value >= 100 ? Math.round(value) : Math.round(value * 10) / 10}%`;
150
155
  }
151
156
  /** 「对话管理」页面的样式表(官方 --dsw-* 变量 + 我们的 css-in-js 层)。
152
- * 页面根是纵向 flex + 整页滚动:除列表外的项都 `flex: none`,否则内容变长时
153
- * flex 会把这些项压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
157
+ * 页面根是纵向 flex 且**自己不滚**:页头(标题 / tab 条 / 导入 / 清理)与搜索行固定在顶部,
158
+ * 只有 `scroll` 那层随内容滚。除滚动层外的项都 `flex: none`,否则内容变长时 flex 会把这些项
159
+ * 压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
154
160
  const styles = {
155
161
  page: {
156
162
  display: "flex",
@@ -160,9 +166,17 @@ window.__ModuleLoader__.load({
160
166
  width: "100%",
161
167
  height: "100%",
162
168
  padding: "24px 28px",
163
- overflow: "auto",
169
+ overflow: "hidden",
164
170
  color: "var(--dsw-alias-label-primary)"
165
171
  },
172
+ scroll: {
173
+ display: "flex",
174
+ flexDirection: "column",
175
+ gap: "12px",
176
+ flex: "1 1 auto",
177
+ minHeight: "0",
178
+ overflowY: "auto"
179
+ },
166
180
  header: {
167
181
  display: "flex",
168
182
  alignItems: "center",
@@ -585,175 +599,182 @@ window.__ModuleLoader__.load({
585
599
  })
586
600
  ]
587
601
  }),
588
- view === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageView, {
589
- report: usage,
590
- loading: usageLoading,
591
- error: usageError,
592
- tab: usageTab,
593
- onTab: setUsageTab,
594
- range: usageRange,
595
- onRange: requestUsage,
596
- t
597
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
598
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
599
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.filters),
600
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
601
- className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.search),
602
- "data-filter": "search",
603
- type: "search",
604
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
605
- value: query,
606
- placeholder: t("search"),
607
- "aria-label": t("search"),
608
- onChange: (event) => {
609
- setQuery(event.currentTarget.value);
602
+ view === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
603
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.scroll),
604
+ "data-scroll": "page",
605
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageView, {
606
+ report: usage,
607
+ loading: usageLoading,
608
+ error: usageError,
609
+ tab: usageTab,
610
+ onTab: setUsageTab,
611
+ range: usageRange,
612
+ onRange: requestUsage,
613
+ t
614
+ })
615
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
616
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.filters),
617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
618
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.search),
619
+ "data-filter": "search",
620
+ type: "search",
621
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
622
+ value: query,
623
+ placeholder: t("search"),
624
+ "aria-label": t("search"),
625
+ onChange: (event) => {
626
+ setQuery(event.currentTarget.value);
627
+ setPage(1);
628
+ }
629
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
630
+ "data-filter": "subagents",
631
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
632
+ checked: showSubagents,
633
+ label: t("showSubagents"),
634
+ onChange: (next) => {
635
+ setShowSubagents(next);
610
636
  setPage(1);
611
637
  }
612
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
613
- "data-filter": "subagents",
614
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
615
- checked: showSubagents,
616
- label: t("showSubagents"),
617
- onChange: (next) => {
618
- setShowSubagents(next);
619
- setPage(1);
620
- }
621
- })
622
- })]
623
- }),
624
- notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
625
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
626
- "data-notice": "result",
627
- children: notice
628
- }),
629
- failure === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
630
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.failure),
631
- "data-failure": "result",
632
- role: "alert",
633
- children: failure
634
- }),
635
- listed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
636
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
637
- "data-status": "empty",
638
- children: t("empty")
639
- }) : null,
640
- listed.length > 0 && matched.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
641
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
642
- "data-status": "empty-search",
643
- children: t("emptySearch")
644
- }) : null,
645
- visible.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
646
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
647
- children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
648
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
649
- "data-session-id": String(row.id),
650
- "data-archived": row.archived ? "true" : "false",
651
- "data-subagent": row.subagent ? "true" : "false",
652
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
653
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.identity),
638
+ })
639
+ })]
640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.scroll),
642
+ "data-scroll": "page",
643
+ children: [
644
+ notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
645
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
646
+ "data-notice": "result",
647
+ children: notice
648
+ }),
649
+ failure === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
650
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.failure),
651
+ "data-failure": "result",
652
+ role: "alert",
653
+ children: failure
654
+ }),
655
+ listed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
656
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
657
+ "data-status": "empty",
658
+ children: t("empty")
659
+ }) : null,
660
+ listed.length > 0 && matched.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
661
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
662
+ "data-status": "empty-search",
663
+ children: t("emptySearch")
664
+ }) : null,
665
+ visible.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
666
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
667
+ children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
668
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
669
+ "data-session-id": String(row.id),
670
+ "data-archived": row.archived ? "true" : "false",
671
+ "data-subagent": row.subagent ? "true" : "false",
654
672
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
655
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLine),
673
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.identity),
674
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
675
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLine),
676
+ children: [
677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
678
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowTitle),
679
+ children: row.title
680
+ }),
681
+ row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
682
+ tone: "neutral",
683
+ children: t("archived")
684
+ }) : null,
685
+ row.subagent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
686
+ tone: "quiet",
687
+ children: t("subagent")
688
+ }) : null
689
+ ]
690
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
691
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.meta),
692
+ children: [row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")
693
+ })]
694
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
695
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.actions),
656
696
  children: [
657
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
658
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowTitle),
659
- children: row.title
697
+ row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
698
+ variant: "outline",
699
+ size: "sm",
700
+ "data-action": "unarchive",
701
+ "aria-label": t("unarchiveNamed", { title: row.title }),
702
+ onClick: () => {
703
+ run(unarchive(row.id));
704
+ },
705
+ children: t("unarchive")
706
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
707
+ variant: "outline",
708
+ size: "sm",
709
+ "data-action": "archive",
710
+ "aria-label": t("archiveNamed", { title: row.title }),
711
+ onClick: () => {
712
+ run(archive(row.id));
713
+ },
714
+ children: t("archive")
715
+ }),
716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
717
+ variant: "outline",
718
+ size: "sm",
719
+ "data-action": "export",
720
+ "aria-label": t("exportNamed", { title: row.title }),
721
+ onClick: () => {
722
+ run(exportZip(row.id));
723
+ },
724
+ children: t("export")
660
725
  }),
661
- row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
662
- tone: "neutral",
663
- children: t("archived")
664
- }) : null,
665
- row.subagent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
666
- tone: "quiet",
667
- children: t("subagent")
668
- }) : null
726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
727
+ variant: "outline",
728
+ size: "sm",
729
+ disabled: !row.archived,
730
+ "data-action": "remove",
731
+ "aria-label": t("removeNamed", { title: row.title }),
732
+ onClick: () => {
733
+ setFailure(null);
734
+ setNotice(null);
735
+ setConfirming(row);
736
+ },
737
+ children: t("remove")
738
+ })
669
739
  ]
670
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
671
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.meta),
672
- children: [row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")
673
740
  })]
674
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
675
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.actions),
676
- children: [
677
- row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
678
- variant: "outline",
679
- size: "sm",
680
- "data-action": "unarchive",
681
- "aria-label": t("unarchiveNamed", { title: row.title }),
682
- onClick: () => {
683
- run(unarchive(row.id));
684
- },
685
- children: t("unarchive")
686
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
687
- variant: "outline",
688
- size: "sm",
689
- "data-action": "archive",
690
- "aria-label": t("archiveNamed", { title: row.title }),
691
- onClick: () => {
692
- run(archive(row.id));
693
- },
694
- children: t("archive")
695
- }),
696
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
697
- variant: "outline",
698
- size: "sm",
699
- "data-action": "export",
700
- "aria-label": t("exportNamed", { title: row.title }),
701
- onClick: () => {
702
- run(exportZip(row.id));
703
- },
704
- children: t("export")
705
- }),
706
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
707
- variant: "outline",
708
- size: "sm",
709
- disabled: !row.archived,
710
- "data-action": "remove",
711
- "aria-label": t("removeNamed", { title: row.title }),
712
- onClick: () => {
713
- setFailure(null);
714
- setNotice(null);
715
- setConfirming(row);
716
- },
717
- children: t("remove")
741
+ }, row.id))
742
+ }) : null,
743
+ matched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
744
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.pagination),
745
+ "data-pagination": "",
746
+ "data-page-current": currentPage,
747
+ "data-page-total": pageCount,
748
+ children: [
749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
750
+ variant: "ghost",
751
+ size: "sm",
752
+ disabled: currentPage <= 1,
753
+ onClick: () => {
754
+ setPage(currentPage - 1);
755
+ },
756
+ children: t("page.previous")
757
+ }),
758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
759
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.paginationLabel),
760
+ children: t("page.label", {
761
+ page: currentPage,
762
+ total: pageCount
718
763
  })
719
- ]
720
- })]
721
- }, row.id))
722
- }) : null,
723
- matched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
724
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.pagination),
725
- "data-pagination": "",
726
- "data-page-current": currentPage,
727
- "data-page-total": pageCount,
728
- children: [
729
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
730
- variant: "ghost",
731
- size: "sm",
732
- disabled: currentPage <= 1,
733
- onClick: () => {
734
- setPage(currentPage - 1);
735
- },
736
- children: t("page.previous")
737
- }),
738
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
739
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.paginationLabel),
740
- children: t("page.label", {
741
- page: currentPage,
742
- total: pageCount
764
+ }),
765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
766
+ variant: "ghost",
767
+ size: "sm",
768
+ disabled: currentPage >= pageCount,
769
+ onClick: () => {
770
+ setPage(currentPage + 1);
771
+ },
772
+ children: t("page.next")
743
773
  })
744
- }),
745
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
746
- variant: "ghost",
747
- size: "sm",
748
- disabled: currentPage >= pageCount,
749
- onClick: () => {
750
- setPage(currentPage + 1);
751
- },
752
- children: t("page.next")
753
- })
754
- ]
755
- }) : null
756
- ] }),
774
+ ]
775
+ }) : null
776
+ ]
777
+ })] }),
757
778
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
758
779
  open: confirmed !== null,
759
780
  onClose: () => {
@@ -844,8 +865,12 @@ window.__ModuleLoader__.load({
844
865
  if (total <= 0) return 0;
845
866
  return totals.cacheReadTokens / total * 100;
846
867
  }
847
- /** 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理、事件——没有合计项。 */
848
- function usageMetrics(totals, t) {
868
+ /**
869
+ * 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理,
870
+ * 以及活动计数(轮次 / 步骤 / 用户输入 / 工具调用)——没有合计项。
871
+ * 活动计数在按模型的行上不显示(事件没有模型归属,见 `withActivity`)。
872
+ */
873
+ function usageMetrics(totals, t, options = {}) {
849
874
  return [
850
875
  {
851
876
  key: "input",
@@ -877,12 +902,32 @@ window.__ModuleLoader__.load({
877
902
  value: totals.reasoningTokens,
878
903
  kind: "tokens"
879
904
  },
880
- {
881
- key: "events",
882
- label: t("usage.events"),
883
- value: totals.events,
884
- kind: "tokens"
885
- }
905
+ ...options.withActivity === false ? [] : [
906
+ {
907
+ key: "turns",
908
+ label: t("usage.turns"),
909
+ value: totals.turns,
910
+ kind: "count"
911
+ },
912
+ {
913
+ key: "steps",
914
+ label: t("usage.steps"),
915
+ value: totals.steps,
916
+ kind: "count"
917
+ },
918
+ {
919
+ key: "userInputs",
920
+ label: t("usage.userInputs"),
921
+ value: totals.userInputs,
922
+ kind: "count"
923
+ },
924
+ {
925
+ key: "toolCalls",
926
+ label: t("usage.toolCalls"),
927
+ value: totals.toolCalls,
928
+ kind: "count"
929
+ }
930
+ ]
886
931
  ];
887
932
  }
888
933
  /** 折叠行的排序口径(不显示):输入(含缓存)+ 输出。 */
@@ -891,7 +936,10 @@ window.__ModuleLoader__.load({
891
936
  }
892
937
  function emptyTotals() {
893
938
  return {
894
- events: 0,
939
+ turns: 0,
940
+ steps: 0,
941
+ userInputs: 0,
942
+ toolCalls: 0,
895
943
  inputTokens: 0,
896
944
  outputTokens: 0,
897
945
  cacheReadTokens: 0,
@@ -909,7 +957,6 @@ window.__ModuleLoader__.load({
909
957
  label: key,
910
958
  totals: emptyTotals()
911
959
  };
912
- row.totals.events += bucket.events;
913
960
  row.totals.inputTokens += bucket.inputTokens;
914
961
  row.totals.outputTokens += bucket.outputTokens;
915
962
  row.totals.cacheReadTokens += bucket.cacheReadTokens;
@@ -920,7 +967,7 @@ window.__ModuleLoader__.load({
920
967
  return [...folded.values()].sort((left, right) => sortWeight(right.totals) - sortWeight(left.totals));
921
968
  }
922
969
  /** 一行用量:label 在上,下面是横向排布的单项。 */
923
- function UsageRow({ rowKey, label, totals, t }) {
970
+ function UsageRow({ rowKey, label, totals, t, withActivity = true }) {
924
971
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
925
972
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageRow),
926
973
  "data-usage-key": rowKey,
@@ -929,7 +976,7 @@ window.__ModuleLoader__.load({
929
976
  children: label
930
977
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
931
978
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetrics),
932
- children: usageMetrics(totals, t).map((metric) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
979
+ children: usageMetrics(totals, t, { withActivity }).map((metric) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
933
980
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetric),
934
981
  "data-usage-cell": metric.key,
935
982
  "data-usage-value": metric.value,
@@ -938,13 +985,13 @@ window.__ModuleLoader__.load({
938
985
  children: metric.label
939
986
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
940
987
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetricValue),
941
- children: metric.kind === "percent" ? formatPercent(metric.value) : formatTokens(metric.value)
988
+ children: metric.kind === "percent" ? formatPercent(metric.value) : metric.kind === "count" ? formatCount(metric.value) : formatTokens(metric.value)
942
989
  })]
943
990
  }, metric.key))
944
991
  })]
945
992
  });
946
993
  }
947
- function UsageList({ rows, t }) {
994
+ function UsageList({ rows, t, withActivity = true }) {
948
995
  if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
949
996
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
950
997
  "data-usage-status": "empty",
@@ -956,7 +1003,8 @@ window.__ModuleLoader__.load({
956
1003
  rowKey: row.key,
957
1004
  label: row.label,
958
1005
  totals: row.totals,
959
- t
1006
+ t,
1007
+ withActivity
960
1008
  }, row.key))
961
1009
  });
962
1010
  }
@@ -1046,7 +1094,8 @@ window.__ModuleLoader__.load({
1046
1094
  t
1047
1095
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageList, {
1048
1096
  rows,
1049
- t
1097
+ t,
1098
+ withActivity: tab === "sessions"
1050
1099
  })
1051
1100
  ]
1052
1101
  });
@@ -1090,7 +1139,10 @@ window.__ModuleLoader__.load({
1090
1139
  "usage.cacheRate": "缓存命中率",
1091
1140
  "usage.reasoning": "推理",
1092
1141
  "usage.total": "合计",
1093
- "usage.events": "事件",
1142
+ "usage.turns": "轮次",
1143
+ "usage.steps": "步骤",
1144
+ "usage.userInputs": "用户输入",
1145
+ "usage.toolCalls": "工具调用",
1094
1146
  "usage.subagentOnly": "其中子代理",
1095
1147
  "usage.unknownModel": "未知模型",
1096
1148
  archive: "归档",
@@ -1162,7 +1214,10 @@ window.__ModuleLoader__.load({
1162
1214
  "usage.cacheRate": "Cache hit rate",
1163
1215
  "usage.reasoning": "Reasoning",
1164
1216
  "usage.total": "Total",
1165
- "usage.events": "Events",
1217
+ "usage.turns": "Turns",
1218
+ "usage.steps": "Steps",
1219
+ "usage.userInputs": "User inputs",
1220
+ "usage.toolCalls": "Tool calls",
1166
1221
  "usage.subagentOnly": "Subagent share",
1167
1222
  "usage.unknownModel": "Unknown model",
1168
1223
  archive: "Archive",
@@ -1225,10 +1280,13 @@ window.__ModuleLoader__.load({
1225
1280
  }), "ui-conversation-manager: dictionaries");
1226
1281
  const t = ctx.locale.bind(NS);
1227
1282
  const sessions = ctx.get("sessions");
1283
+ const refreshList = sessions.refresh;
1228
1284
  const controller = new ConversationManagerController({
1229
1285
  archiveSession: (sessionId) => ctx.uiWorkspace.archiveSession(sessionId),
1230
1286
  unarchiveSession: (sessionId) => ctx.uiWorkspace.unarchiveSession(sessionId),
1231
- refresh: () => sessions.refresh()
1287
+ refresh: async () => {
1288
+ await refreshList?.call(sessions);
1289
+ }
1232
1290
  });
1233
1291
  ctx.slots.inject("main", () => ctx.slots.register({
1234
1292
  name: "main",
package/dist/client.d.cts CHANGED
@@ -35,7 +35,10 @@ declare const zh: {
35
35
  "usage.cacheRate": string;
36
36
  "usage.reasoning": string;
37
37
  "usage.total": string;
38
- "usage.events": string;
38
+ "usage.turns": string;
39
+ "usage.steps": string;
40
+ "usage.userInputs": string;
41
+ "usage.toolCalls": string;
39
42
  "usage.subagentOnly": string;
40
43
  "usage.unknownModel": string;
41
44
  archive: string;
@@ -3023,21 +3026,32 @@ declare class SessionStore extends Service {
3023
3026
  //#region src/client/controller.d.ts
3024
3027
  /** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
3025
3028
  type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
3026
- /** 一段用量合计(与 session-rdb `./usage` 的回报结构镜像)。 */
3027
- interface UsageTotals {
3028
- events: number;
3029
+ /** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
3030
+ interface UsageActivityTotals {
3031
+ turns: number;
3032
+ steps: number;
3033
+ userInputs: number;
3034
+ toolCalls: number;
3035
+ }
3036
+ /** 一段用量合计:token 用量 + 活动计数。 */
3037
+ interface UsageTotals extends UsageActivityTotals {
3029
3038
  inputTokens: number;
3030
3039
  outputTokens: number;
3031
3040
  cacheReadTokens: number;
3032
3041
  reasoningTokens: number;
3033
3042
  totalTokens: number;
3034
3043
  }
3035
- /** 一天 × 一个模型 × 是否子代理 的用量桶。 */
3036
- interface UsageBucket extends UsageTotals {
3044
+ /** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
3045
+ interface UsageBucket {
3037
3046
  day: string;
3038
3047
  provider: string | null;
3039
3048
  model: string | null;
3040
3049
  subagent: boolean;
3050
+ inputTokens: number;
3051
+ outputTokens: number;
3052
+ cacheReadTokens: number;
3053
+ reasoningTokens: number;
3054
+ totalTokens: number;
3041
3055
  }
3042
3056
  /** 一条会话的用量行。 */
3043
3057
  interface UsageSessionRow extends UsageTotals {
package/dist/client.d.mts CHANGED
@@ -35,7 +35,10 @@ declare const zh: {
35
35
  "usage.cacheRate": string;
36
36
  "usage.reasoning": string;
37
37
  "usage.total": string;
38
- "usage.events": string;
38
+ "usage.turns": string;
39
+ "usage.steps": string;
40
+ "usage.userInputs": string;
41
+ "usage.toolCalls": string;
39
42
  "usage.subagentOnly": string;
40
43
  "usage.unknownModel": string;
41
44
  archive: string;
@@ -3023,21 +3026,32 @@ declare class SessionStore extends Service {
3023
3026
  //#region src/client/controller.d.ts
3024
3027
  /** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
3025
3028
  type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
3026
- /** 一段用量合计(与 session-rdb `./usage` 的回报结构镜像)。 */
3027
- interface UsageTotals {
3028
- events: number;
3029
+ /** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
3030
+ interface UsageActivityTotals {
3031
+ turns: number;
3032
+ steps: number;
3033
+ userInputs: number;
3034
+ toolCalls: number;
3035
+ }
3036
+ /** 一段用量合计:token 用量 + 活动计数。 */
3037
+ interface UsageTotals extends UsageActivityTotals {
3029
3038
  inputTokens: number;
3030
3039
  outputTokens: number;
3031
3040
  cacheReadTokens: number;
3032
3041
  reasoningTokens: number;
3033
3042
  totalTokens: number;
3034
3043
  }
3035
- /** 一天 × 一个模型 × 是否子代理 的用量桶。 */
3036
- interface UsageBucket extends UsageTotals {
3044
+ /** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
3045
+ interface UsageBucket {
3037
3046
  day: string;
3038
3047
  provider: string | null;
3039
3048
  model: string | null;
3040
3049
  subagent: boolean;
3050
+ inputTokens: number;
3051
+ outputTokens: number;
3052
+ cacheReadTokens: number;
3053
+ reasoningTokens: number;
3054
+ totalTokens: number;
3041
3055
  }
3042
3056
  /** 一条会话的用量行。 */
3043
3057
  interface UsageSessionRow extends UsageTotals {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/ui-conversation-manager",
3
- "version": "0.0.2-alpha.3",
3
+ "version": "0.0.2-alpha.5",
4
4
  "description": "「对话管理」全局面板页(浏览器半):已归档会话的搜索、取消归档、删除,以及导入为新会话。",
5
5
  "keywords": [
6
6
  "archived-sessions",
@@ -48,7 +48,7 @@
48
48
  "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
49
49
  "@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.2",
50
50
  "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
51
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.3"
51
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5"
52
52
  },
53
53
  "dsh": {
54
54
  "client": {
@@ -6,8 +6,9 @@ const spin = styling.keyframes({
6
6
  });
7
7
 
8
8
  /** 「对话管理」页面的样式表(官方 --dsw-* 变量 + 我们的 css-in-js 层)。
9
- * 页面根是纵向 flex + 整页滚动:除列表外的项都 `flex: none`,否则内容变长时
10
- * flex 会把这些项压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
9
+ * 页面根是纵向 flex 且**自己不滚**:页头(标题 / tab 条 / 导入 / 清理)与搜索行固定在顶部,
10
+ * 只有 `scroll` 那层随内容滚。除滚动层外的项都 `flex: none`,否则内容变长时 flex 会把这些项
11
+ * 压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
11
12
  export const styles = {
12
13
  page: {
13
14
  display: "flex",
@@ -17,9 +18,18 @@ export const styles = {
17
18
  width: "100%",
18
19
  height: "100%",
19
20
  padding: "24px 28px",
20
- overflow: "auto",
21
+ overflow: "hidden",
21
22
  color: "var(--dsw-alias-label-primary)",
22
23
  },
24
+ // 页面里唯一的滚动容器:提示 / 空态 / 列表 / 分页(或 usage 视图)都在它里面。
25
+ scroll: {
26
+ display: "flex",
27
+ flexDirection: "column",
28
+ gap: "12px",
29
+ flex: "1 1 auto",
30
+ minHeight: "0",
31
+ overflowY: "auto",
32
+ },
23
33
  header: {
24
34
  display: "flex",
25
35
  alignItems: "center",
@@ -21,7 +21,7 @@ import {
21
21
  type UsageRangeKey,
22
22
  type UsageTotals,
23
23
  } from "./controller.ts";
24
- import { formatPercent, formatTokens } from "./format.ts";
24
+ import { formatCount, formatPercent, formatTokens } from "./format.ts";
25
25
  import { styles } from "./ConversationManagerPage.styles.ts";
26
26
 
27
27
  /** 一页的行数(会话列表)。 */
@@ -286,16 +286,18 @@ export function ConversationManagerPage({
286
286
  />
287
287
  </div>
288
288
  {view === "usage" ? (
289
- <UsageView
290
- report={usage}
291
- loading={usageLoading}
292
- error={usageError}
293
- tab={usageTab}
294
- onTab={setUsageTab}
295
- range={usageRange}
296
- onRange={requestUsage}
297
- t={t}
298
- />
289
+ <div {...styling.props(styles.scroll)} data-scroll="page">
290
+ <UsageView
291
+ report={usage}
292
+ loading={usageLoading}
293
+ error={usageError}
294
+ tab={usageTab}
295
+ onTab={setUsageTab}
296
+ range={usageRange}
297
+ onRange={requestUsage}
298
+ t={t}
299
+ />
300
+ </div>
299
301
  ) : (
300
302
  <>
301
303
  <div {...styling.props(styles.filters)}>
@@ -323,134 +325,136 @@ export function ConversationManagerPage({
323
325
  />
324
326
  </span>
325
327
  </div>
326
- {notice === null ? null : (
327
- <p {...styling.props(styles.status)} data-notice="result">
328
- {notice}
329
- </p>
330
- )}
331
- {failure === null ? null : (
332
- <p {...styling.props(styles.failure)} data-failure="result" role="alert">
333
- {failure}
334
- </p>
335
- )}
336
- {listed.length === 0 ? (
337
- <p {...styling.props(styles.status)} data-status="empty">
338
- {t("empty")}
339
- </p>
340
- ) : null}
341
- {listed.length > 0 && matched.length === 0 ? (
342
- <p {...styling.props(styles.status)} data-status="empty-search">
343
- {t("emptySearch")}
344
- </p>
345
- ) : null}
346
- {visible.length > 0 ? (
347
- <ul {...styling.props(styles.list)}>
348
- {visible.map((row) => (
349
- <li
350
- key={row.id}
351
- {...styling.props(styles.row)}
352
- data-session-id={String(row.id)}
353
- data-archived={row.archived ? "true" : "false"}
354
- data-subagent={row.subagent ? "true" : "false"}
355
- >
356
- <span {...styling.props(styles.identity)}>
357
- <span {...styling.props(styles.titleLine)}>
358
- <span {...styling.props(styles.rowTitle)}>{row.title}</span>
359
- {row.archived ? <Tag tone="neutral">{t("archived")}</Tag> : null}
360
- {row.subagent ? <Tag tone="quiet">{t("subagent")}</Tag> : null}
361
- </span>
362
- <span {...styling.props(styles.meta)}>
363
- {[row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")}
328
+ <div {...styling.props(styles.scroll)} data-scroll="page">
329
+ {notice === null ? null : (
330
+ <p {...styling.props(styles.status)} data-notice="result">
331
+ {notice}
332
+ </p>
333
+ )}
334
+ {failure === null ? null : (
335
+ <p {...styling.props(styles.failure)} data-failure="result" role="alert">
336
+ {failure}
337
+ </p>
338
+ )}
339
+ {listed.length === 0 ? (
340
+ <p {...styling.props(styles.status)} data-status="empty">
341
+ {t("empty")}
342
+ </p>
343
+ ) : null}
344
+ {listed.length > 0 && matched.length === 0 ? (
345
+ <p {...styling.props(styles.status)} data-status="empty-search">
346
+ {t("emptySearch")}
347
+ </p>
348
+ ) : null}
349
+ {visible.length > 0 ? (
350
+ <ul {...styling.props(styles.list)}>
351
+ {visible.map((row) => (
352
+ <li
353
+ key={row.id}
354
+ {...styling.props(styles.row)}
355
+ data-session-id={String(row.id)}
356
+ data-archived={row.archived ? "true" : "false"}
357
+ data-subagent={row.subagent ? "true" : "false"}
358
+ >
359
+ <span {...styling.props(styles.identity)}>
360
+ <span {...styling.props(styles.titleLine)}>
361
+ <span {...styling.props(styles.rowTitle)}>{row.title}</span>
362
+ {row.archived ? <Tag tone="neutral">{t("archived")}</Tag> : null}
363
+ {row.subagent ? <Tag tone="quiet">{t("subagent")}</Tag> : null}
364
+ </span>
365
+ <span {...styling.props(styles.meta)}>
366
+ {[row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")}
367
+ </span>
364
368
  </span>
365
- </span>
366
- <span {...styling.props(styles.actions)}>
367
- {row.archived ? (
369
+ <span {...styling.props(styles.actions)}>
370
+ {row.archived ? (
371
+ <Button
372
+ variant="outline"
373
+ size="sm"
374
+ data-action="unarchive"
375
+ aria-label={t("unarchiveNamed", { title: row.title })}
376
+ onClick={() => {
377
+ run(unarchive(row.id));
378
+ }}
379
+ >
380
+ {t("unarchive")}
381
+ </Button>
382
+ ) : (
383
+ <Button
384
+ variant="outline"
385
+ size="sm"
386
+ data-action="archive"
387
+ aria-label={t("archiveNamed", { title: row.title })}
388
+ onClick={() => {
389
+ run(archive(row.id));
390
+ }}
391
+ >
392
+ {t("archive")}
393
+ </Button>
394
+ )}
368
395
  <Button
369
396
  variant="outline"
370
397
  size="sm"
371
- data-action="unarchive"
372
- aria-label={t("unarchiveNamed", { title: row.title })}
398
+ data-action="export"
399
+ aria-label={t("exportNamed", { title: row.title })}
373
400
  onClick={() => {
374
- run(unarchive(row.id));
401
+ run(exportZip(row.id));
375
402
  }}
376
403
  >
377
- {t("unarchive")}
404
+ {t("export")}
378
405
  </Button>
379
- ) : (
380
406
  <Button
381
407
  variant="outline"
382
408
  size="sm"
383
- data-action="archive"
384
- aria-label={t("archiveNamed", { title: row.title })}
409
+ disabled={!row.archived}
410
+ data-action="remove"
411
+ aria-label={t("removeNamed", { title: row.title })}
385
412
  onClick={() => {
386
- run(archive(row.id));
413
+ setFailure(null);
414
+ setNotice(null);
415
+ setConfirming(row);
387
416
  }}
388
417
  >
389
- {t("archive")}
418
+ {t("remove")}
390
419
  </Button>
391
- )}
392
- <Button
393
- variant="outline"
394
- size="sm"
395
- data-action="export"
396
- aria-label={t("exportNamed", { title: row.title })}
397
- onClick={() => {
398
- run(exportZip(row.id));
399
- }}
400
- >
401
- {t("export")}
402
- </Button>
403
- <Button
404
- variant="outline"
405
- size="sm"
406
- disabled={!row.archived}
407
- data-action="remove"
408
- aria-label={t("removeNamed", { title: row.title })}
409
- onClick={() => {
410
- setFailure(null);
411
- setNotice(null);
412
- setConfirming(row);
413
- }}
414
- >
415
- {t("remove")}
416
- </Button>
417
- </span>
418
- </li>
419
- ))}
420
- </ul>
421
- ) : null}
422
- {matched.length > 0 ? (
423
- <div
424
- {...styling.props(styles.pagination)}
425
- data-pagination=""
426
- data-page-current={currentPage}
427
- data-page-total={pageCount}
428
- >
429
- <Button
430
- variant="ghost"
431
- size="sm"
432
- disabled={currentPage <= 1}
433
- onClick={() => {
434
- setPage(currentPage - 1);
435
- }}
436
- >
437
- {t("page.previous")}
438
- </Button>
439
- <span {...styling.props(styles.paginationLabel)}>
440
- {t("page.label", { page: currentPage, total: pageCount })}
441
- </span>
442
- <Button
443
- variant="ghost"
444
- size="sm"
445
- disabled={currentPage >= pageCount}
446
- onClick={() => {
447
- setPage(currentPage + 1);
448
- }}
420
+ </span>
421
+ </li>
422
+ ))}
423
+ </ul>
424
+ ) : null}
425
+ {matched.length > 0 ? (
426
+ <div
427
+ {...styling.props(styles.pagination)}
428
+ data-pagination=""
429
+ data-page-current={currentPage}
430
+ data-page-total={pageCount}
449
431
  >
450
- {t("page.next")}
451
- </Button>
452
- </div>
453
- ) : null}
432
+ <Button
433
+ variant="ghost"
434
+ size="sm"
435
+ disabled={currentPage <= 1}
436
+ onClick={() => {
437
+ setPage(currentPage - 1);
438
+ }}
439
+ >
440
+ {t("page.previous")}
441
+ </Button>
442
+ <span {...styling.props(styles.paginationLabel)}>
443
+ {t("page.label", { page: currentPage, total: pageCount })}
444
+ </span>
445
+ <Button
446
+ variant="ghost"
447
+ size="sm"
448
+ disabled={currentPage >= pageCount}
449
+ onClick={() => {
450
+ setPage(currentPage + 1);
451
+ }}
452
+ >
453
+ {t("page.next")}
454
+ </Button>
455
+ </div>
456
+ ) : null}
457
+ </div>
454
458
  </>
455
459
  )}
456
460
  <Modal
@@ -553,9 +557,9 @@ function rangeLabel(range: UsageRange, t: Translate): string {
553
557
  interface UsageMetric {
554
558
  key: string;
555
559
  label: string;
556
- /** 原始值:token 数,或百分点(`percent` 项)。 */
560
+ /** 原始值:token 数、计数,或百分点(`percent` 项)。 */
557
561
  value: number;
558
- kind: "tokens" | "percent";
562
+ kind: "tokens" | "count" | "percent";
559
563
  }
560
564
 
561
565
  /** 缓存命中率(百分点):缓存输入占总输入(含缓存)的比例。 */
@@ -565,8 +569,16 @@ function cacheHitPercent(totals: UsageTotals): number {
565
569
  return (totals.cacheReadTokens / total) * 100;
566
570
  }
567
571
 
568
- /** 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理、事件——没有合计项。 */
569
- function usageMetrics(totals: UsageTotals, t: Translate): UsageMetric[] {
572
+ /**
573
+ * 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理,
574
+ * 以及活动计数(轮次 / 步骤 / 用户输入 / 工具调用)——没有合计项。
575
+ * 活动计数在按模型的行上不显示(事件没有模型归属,见 `withActivity`)。
576
+ */
577
+ function usageMetrics(
578
+ totals: UsageTotals,
579
+ t: Translate,
580
+ options: { withActivity?: boolean } = {},
581
+ ): UsageMetric[] {
570
582
  return [
571
583
  {
572
584
  key: "input",
@@ -593,7 +605,24 @@ function usageMetrics(totals: UsageTotals, t: Translate): UsageMetric[] {
593
605
  value: totals.reasoningTokens,
594
606
  kind: "tokens",
595
607
  },
596
- { key: "events", label: t("usage.events"), value: totals.events, kind: "tokens" },
608
+ ...(options.withActivity === false
609
+ ? []
610
+ : [
611
+ { key: "turns", label: t("usage.turns"), value: totals.turns, kind: "count" as const },
612
+ { key: "steps", label: t("usage.steps"), value: totals.steps, kind: "count" as const },
613
+ {
614
+ key: "userInputs",
615
+ label: t("usage.userInputs"),
616
+ value: totals.userInputs,
617
+ kind: "count" as const,
618
+ },
619
+ {
620
+ key: "toolCalls",
621
+ label: t("usage.toolCalls"),
622
+ value: totals.toolCalls,
623
+ kind: "count" as const,
624
+ },
625
+ ]),
597
626
  ];
598
627
  }
599
628
 
@@ -610,7 +639,10 @@ interface UsageListRow {
610
639
 
611
640
  function emptyTotals(): UsageTotals {
612
641
  return {
613
- events: 0,
642
+ turns: 0,
643
+ steps: 0,
644
+ userInputs: 0,
645
+ toolCalls: 0,
614
646
  inputTokens: 0,
615
647
  outputTokens: 0,
616
648
  cacheReadTokens: 0,
@@ -628,7 +660,6 @@ function foldBuckets(
628
660
  for (const bucket of buckets) {
629
661
  const key = keyOf(bucket);
630
662
  const row = folded.get(key) ?? { key, label: key, totals: emptyTotals() };
631
- row.totals.events += bucket.events;
632
663
  row.totals.inputTokens += bucket.inputTokens;
633
664
  row.totals.outputTokens += bucket.outputTokens;
634
665
  row.totals.cacheReadTokens += bucket.cacheReadTokens;
@@ -647,17 +678,19 @@ function UsageRow({
647
678
  label,
648
679
  totals,
649
680
  t,
681
+ withActivity = true,
650
682
  }: {
651
683
  rowKey: string;
652
684
  label: string;
653
685
  totals: UsageTotals;
654
686
  t: Translate;
687
+ withActivity?: boolean;
655
688
  }): ReactNode {
656
689
  return (
657
690
  <li {...styling.props(styles.usageRow)} data-usage-key={rowKey}>
658
691
  <span {...styling.props(styles.usageRowLabel)}>{label}</span>
659
692
  <span {...styling.props(styles.usageMetrics)}>
660
- {usageMetrics(totals, t).map((metric) => (
693
+ {usageMetrics(totals, t, { withActivity }).map((metric) => (
661
694
  <span
662
695
  key={metric.key}
663
696
  {...styling.props(styles.usageMetric)}
@@ -666,7 +699,11 @@ function UsageRow({
666
699
  >
667
700
  <span {...styling.props(styles.usageMetricLabel)}>{metric.label}</span>
668
701
  <span {...styling.props(styles.usageMetricValue)}>
669
- {metric.kind === "percent" ? formatPercent(metric.value) : formatTokens(metric.value)}
702
+ {metric.kind === "percent"
703
+ ? formatPercent(metric.value)
704
+ : metric.kind === "count"
705
+ ? formatCount(metric.value)
706
+ : formatTokens(metric.value)}
670
707
  </span>
671
708
  </span>
672
709
  ))}
@@ -675,7 +712,15 @@ function UsageRow({
675
712
  );
676
713
  }
677
714
 
678
- function UsageList({ rows, t }: { rows: readonly UsageListRow[]; t: Translate }): ReactNode {
715
+ function UsageList({
716
+ rows,
717
+ t,
718
+ withActivity = true,
719
+ }: {
720
+ rows: readonly UsageListRow[];
721
+ t: Translate;
722
+ withActivity?: boolean;
723
+ }): ReactNode {
679
724
  if (rows.length === 0) {
680
725
  return (
681
726
  <p {...styling.props(styles.status)} data-usage-status="empty">
@@ -686,7 +731,14 @@ function UsageList({ rows, t }: { rows: readonly UsageListRow[]; t: Translate })
686
731
  return (
687
732
  <ul {...styling.props(styles.usageList)}>
688
733
  {rows.map((row) => (
689
- <UsageRow key={row.key} rowKey={row.key} label={row.label} totals={row.totals} t={t} />
734
+ <UsageRow
735
+ key={row.key}
736
+ rowKey={row.key}
737
+ label={row.label}
738
+ totals={row.totals}
739
+ t={t}
740
+ withActivity={withActivity}
741
+ />
690
742
  ))}
691
743
  </ul>
692
744
  );
@@ -794,7 +846,7 @@ function UsageView({
794
846
  {report === null ? null : tab === "overview" ? (
795
847
  <UsageOverview report={report} t={t} />
796
848
  ) : (
797
- <UsageList rows={rows} t={t} />
849
+ <UsageList rows={rows} t={t} withActivity={tab === "sessions"} />
798
850
  )}
799
851
  </div>
800
852
  );
@@ -10,9 +10,16 @@ export const SESSION_USAGE_PATH = "/api/session.usage";
10
10
  /** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
11
11
  export type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
12
12
 
13
- /** 一段用量合计(与 session-rdb `./usage` 的回报结构镜像)。 */
14
- export interface UsageTotals {
15
- events: number;
13
+ /** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
14
+ export interface UsageActivityTotals {
15
+ turns: number;
16
+ steps: number;
17
+ userInputs: number;
18
+ toolCalls: number;
19
+ }
20
+
21
+ /** 一段用量合计:token 用量 + 活动计数。 */
22
+ export interface UsageTotals extends UsageActivityTotals {
16
23
  inputTokens: number;
17
24
  outputTokens: number;
18
25
  cacheReadTokens: number;
@@ -20,12 +27,17 @@ export interface UsageTotals {
20
27
  totalTokens: number;
21
28
  }
22
29
 
23
- /** 一天 × 一个模型 × 是否子代理 的用量桶。 */
24
- export interface UsageBucket extends UsageTotals {
30
+ /** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
31
+ export interface UsageBucket {
25
32
  day: string;
26
33
  provider: string | null;
27
34
  model: string | null;
28
35
  subagent: boolean;
36
+ inputTokens: number;
37
+ outputTokens: number;
38
+ cacheReadTokens: number;
39
+ reasoningTokens: number;
40
+ totalTokens: number;
29
41
  }
30
42
 
31
43
  /** 一条会话的用量行。 */
@@ -14,6 +14,12 @@ export function formatTokens(value: number): string {
14
14
  return `${trim(value / 1_000_000_000)}B`;
15
15
  }
16
16
 
17
+ /** @param value - 计数(轮次 / 步骤 / 用户输入 / 工具调用)。 @returns 千分位原文,例如 `1,024`。 */
18
+ export function formatCount(value: number): string {
19
+ if (!Number.isFinite(value) || value <= 0) return "0";
20
+ return Math.round(value).toLocaleString("en-US");
21
+ }
22
+
17
23
  /** @param value - 百分点(0~100)。 @returns 紧凑百分比,例如 `98.4%`、`100%`。 */
18
24
  export function formatPercent(value: number): string {
19
25
  if (!Number.isFinite(value) || value <= 0) return "0%";
@@ -43,10 +43,15 @@ export function apply(ctx: ClientContext): void {
43
43
  const t = ctx.locale.bind(NS);
44
44
  // ctx.sessions 的类型被别的 client 半的声明占住(SessionStore),按既有做法从服务面取。
45
45
  const sessions = ctx.get("sessions") as unknown as ISessions;
46
+ // 列表刷新入口在运行时可能缺席(`refresh` 是类型面的方法,实测并非每个运行时都提供):
47
+ // 有就调、没有就跳过——列表本身由 host 的 `api-session/*` 推送驱动,不会因此停在旧值。
48
+ const refreshList = (sessions as unknown as { refresh?: () => Promise<void> }).refresh;
46
49
  const controller = new ConversationManagerController({
47
50
  archiveSession: (sessionId) => ctx.uiWorkspace.archiveSession(sessionId),
48
51
  unarchiveSession: (sessionId) => ctx.uiWorkspace.unarchiveSession(sessionId),
49
- refresh: () => sessions.refresh(),
52
+ refresh: async () => {
53
+ await refreshList?.call(sessions);
54
+ },
50
55
  });
51
56
 
52
57
  ctx.slots.inject("main", () =>
@@ -31,7 +31,10 @@ export const zh = {
31
31
  "usage.cacheRate": "缓存命中率",
32
32
  "usage.reasoning": "推理",
33
33
  "usage.total": "合计",
34
- "usage.events": "事件",
34
+ "usage.turns": "轮次",
35
+ "usage.steps": "步骤",
36
+ "usage.userInputs": "用户输入",
37
+ "usage.toolCalls": "工具调用",
35
38
  "usage.subagentOnly": "其中子代理",
36
39
  "usage.unknownModel": "未知模型",
37
40
  archive: "归档",
@@ -108,7 +111,10 @@ export const en = {
108
111
  "usage.cacheRate": "Cache hit rate",
109
112
  "usage.reasoning": "Reasoning",
110
113
  "usage.total": "Total",
111
- "usage.events": "Events",
114
+ "usage.turns": "Turns",
115
+ "usage.steps": "Steps",
116
+ "usage.userInputs": "User inputs",
117
+ "usage.toolCalls": "Tool calls",
112
118
  "usage.subagentOnly": "Subagent share",
113
119
  "usage.unknownModel": "Unknown model",
114
120
  archive: "Archive",