@dungsil/dsh-plugin-ui-usage 0.1.1 → 0.3.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.
package/dist/client.js CHANGED
@@ -1,8 +1,8 @@
1
1
  window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-usage", factory(require) { const module = { exports: {} }; const exports = module.exports;
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  let react = require("react");
4
- let react_jsx_runtime = require("react/jsx-runtime");
5
4
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
6
  // ../../tools/ui/src/client.ts
7
7
  function useLocaleSnapshot(locale) {
8
8
  const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
@@ -16,6 +16,53 @@ function installStyles(css) {
16
16
  document.head.append(style);
17
17
  return () => style.remove();
18
18
  }
19
+ // ../../tools/ui/src/button-picker.tsx
20
+ function ButtonPicker({ label, value, items, onChange, disabled = false, menuClassName, compact, ...buttonProps }) {
21
+ const [open, setOpen] = (0, react.useState)(false);
22
+ (0, react.useEffect)(() => {
23
+ if (disabled) setOpen(false);
24
+ }, [disabled]);
25
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
26
+ open: open && !disabled,
27
+ portal: true,
28
+ compact,
29
+ className: menuClassName,
30
+ selectedId: value,
31
+ items,
32
+ onClose: () => setOpen(false),
33
+ onSelect: (id) => {
34
+ setOpen(false);
35
+ const item = items.find((item) => item.id === id && !item.disabled);
36
+ if (!disabled && item) onChange(item.id);
37
+ },
38
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
39
+ ...buttonProps,
40
+ type: "button",
41
+ "aria-label": label,
42
+ "aria-haspopup": "menu",
43
+ "aria-expanded": open && !disabled,
44
+ disabled,
45
+ onClick: () => setOpen((current) => !current),
46
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: items.find((item) => item.id === value)?.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { "aria-hidden": "true" })]
47
+ })
48
+ });
49
+ }
50
+ // ../../tools/ui/src/page.tsx
51
+ function PluginPage({ children, className = "", contentClassName = "" }) {
52
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
53
+ className: `dsh-plugin-page ${className}`,
54
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55
+ className: `dsh-plugin-page-content ${contentClassName}`,
56
+ children
57
+ })
58
+ });
59
+ }
60
+ // ../../tools/ui/src/page-styles.ts
61
+ const pageStyles = `
62
+ .dsh-plugin-page { box-sizing: border-box; height: 100%; min-height: 0; overflow: auto; padding: 24px; padding-inline-start: max(24px, var(--dsh-frame-leading-clearance, 0px)); }
63
+ .dsh-plugin-page-content { max-width: 960px; margin: 0 auto; }
64
+ .dsh-plugin-page h1 { margin: 0 0 24px; font-size: 20px; line-height: 28px; font-weight: 500; }
65
+ `;
19
66
  // src/copy.ts
20
67
  const copy = {
21
68
  en: {
@@ -39,6 +86,16 @@ const copy = {
39
86
  daily: "Daily tokens",
40
87
  recent: "Daily tokens · last 30 days",
41
88
  models: "By provider / model",
89
+ requests: "Model call log",
90
+ requestScope: "Most recent 100 settled calls in this period, newest first. Times are settlement times. Retries are separate calls. Request and response bodies are not shown.",
91
+ time: "Recorded at",
92
+ session: "Session",
93
+ status: "Status",
94
+ success: "Succeeded",
95
+ failure: "Failed",
96
+ interrupted: "Interrupted",
97
+ unknownStatus: "Unconfirmed",
98
+ notReported: "Not reported",
42
99
  unknown: "Unknown",
43
100
  updated: "Updated",
44
101
  unreported: "Calls without token counts",
@@ -67,6 +124,16 @@ const copy = {
67
124
  daily: "일별 토큰",
68
125
  recent: "일별 토큰 · 최근 30일",
69
126
  models: "제공자 / 모델별",
127
+ requests: "모델 호출 로그",
128
+ requestScope: "선택한 기간에 기록이 확정된 최근 100건을 최신순으로 표시합니다. 시각은 기록 확정 시각이며, 재시도는 각각 표시합니다. 요청·응답 본문은 표시하지 않습니다.",
129
+ time: "기록 시각",
130
+ session: "세션",
131
+ status: "상태",
132
+ success: "성공",
133
+ failure: "실패",
134
+ interrupted: "중단",
135
+ unknownStatus: "미확정",
136
+ notReported: "미보고",
70
137
  unknown: "알 수 없음",
71
138
  updated: "갱신 시각",
72
139
  unreported: "토큰 수가 보고되지 않은 호출",
@@ -95,6 +162,16 @@ const copy = {
95
162
  daily: "每日令牌数",
96
163
  recent: "每日令牌数 · 最近 30 天",
97
164
  models: "按提供方 / 模型",
165
+ requests: "模型调用日志",
166
+ requestScope: "按时间倒序显示所选时间范围内最近 100 次已结算调用。时间为记录结算时间,重试单独列出。不显示请求和响应正文。",
167
+ time: "记录时间",
168
+ session: "会话",
169
+ status: "状态",
170
+ success: "成功",
171
+ failure: "失败",
172
+ interrupted: "已中断",
173
+ unknownStatus: "未确认",
174
+ notReported: "未报告",
98
175
  unknown: "未知",
99
176
  updated: "更新时间",
100
177
  unreported: "未报告令牌数的调用",
@@ -115,10 +192,7 @@ const inject = [
115
192
  "slots"
116
193
  ];
117
194
  const cachedViews = /* @__PURE__ */ new WeakMap();
118
- const styles = `
119
- .dsh-usage-page { box-sizing: border-box; height: 100%; overflow: auto; padding: 24px; padding-inline-start: max(24px, var(--dsh-frame-leading-clearance, 0px)); }
120
- .dsh-usage-page > .dsh-usage { max-width: 960px; margin: 0 auto; }
121
- .dsh-usage h1 { margin: 0 0 24px; font-size: 20px; line-height: 28px; font-weight: 500; }
195
+ const styles = `${pageStyles}
122
196
  .dsh-usage { container-type: inline-size; overflow-wrap: anywhere; color: var(--dsw-alias-label-primary); font: 14px/22px var(--dsw-font-family, system-ui); }
123
197
  .dsh-usage-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
124
198
  .dsh-usage-status { display: flex; align-items: center; gap: 6px; }
@@ -146,6 +220,13 @@ const styles = `
146
220
  .dsh-usage-model-heading > span:first-child { min-width: 0; }
147
221
  .dsh-usage-model-heading > span:last-child { flex: none; font-variant-numeric: tabular-nums; }
148
222
  .dsh-usage-model dl { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-top: 12px; font-size: 12px; }
223
+ .dsh-usage-calls { list-style: none; margin: 0; padding: 0 12px; max-height: 520px; overflow: auto; border: .5px solid var(--dsw-alias-border-l2); border-radius: 8px; }
224
+ .dsh-usage-calls:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
225
+ .dsh-usage-calls .dsh-usage-model:last-child { border-bottom: none; }
226
+ .dsh-usage-calls dl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
227
+ .dsh-usage-call-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 6px; }
228
+ .dsh-usage-call-status { flex: none; font-size: 12px; }
229
+ .dsh-usage-call-status[data-status=failure] { color: var(--dsw-alias-state-error-primary, #b54424); }
149
230
  .dsh-usage [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
150
231
  @container (max-width: 480px) {
151
232
  .dsh-usage-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -160,23 +241,34 @@ function Usage({ ctx }) {
160
241
  const locale = useLocaleSnapshot(ctx.locale);
161
242
  const text = localizedCopy(locale.active);
162
243
  const number = new Intl.NumberFormat(locale.active);
244
+ const tokens = new Intl.NumberFormat(locale.active, {
245
+ notation: "compact",
246
+ maximumFractionDigits: 1
247
+ });
163
248
  const [period, setPeriod] = (0, react.useState)(7);
164
- const [periodOpen, setPeriodOpen] = (0, react.useState)(false);
165
249
  const [refresh, setRefresh] = (0, react.useState)(0);
250
+ const refreshRequested = (0, react.useRef)(false);
166
251
  const connection = ctx.get("connection");
167
252
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
168
253
  const cacheKey = JSON.stringify([period, timeZone]);
169
254
  let cache = cachedViews.get(connection);
170
255
  if (!cache) cachedViews.set(connection, cache = /* @__PURE__ */ new Map());
171
256
  const [view, setView] = (0, react.useState)(() => cache.get(cacheKey));
172
- const [busy, setBusy] = (0, react.useState)(true);
257
+ const [busy, setBusy] = (0, react.useState)(() => !cache.has(cacheKey));
173
258
  const [error, setError] = (0, react.useState)(false);
174
259
  const [selectedDay, setSelectedDay] = (0, react.useState)(() => view?.days.at(-1)?.day ?? "");
175
260
  (0, react.useEffect)(() => {
261
+ const cached = cache.get(cacheKey);
262
+ setError(false);
263
+ setView(cached);
264
+ if (cached && !refreshRequested.current) {
265
+ setBusy(false);
266
+ setSelectedDay((day) => cached.days.some((entry) => entry.day === day) ? day : cached.days.at(-1)?.day ?? "");
267
+ return;
268
+ }
269
+ refreshRequested.current = false;
176
270
  const controller = new AbortController();
177
271
  setBusy(true);
178
- setError(false);
179
- setView(cache.get(cacheKey));
180
272
  (async () => {
181
273
  try {
182
274
  const result = await connection.rpc.call(channel, method, {
@@ -239,31 +331,22 @@ function Usage({ ctx }) {
239
331
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: text.nav }),
240
332
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
241
333
  className: "dsh-usage-toolbar",
242
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
243
- open: periodOpen,
244
- onClose: () => setPeriodOpen(false),
245
- selectedId: String(period),
334
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonPicker, {
335
+ variant: "outline",
336
+ label: text.period,
337
+ value: String(period),
246
338
  items: periods,
247
- onSelect: (id) => {
248
- setPeriodOpen(false);
249
- setPeriod(Number(id));
250
- },
251
- portal: true,
252
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
253
- variant: "outline",
254
- "aria-label": text.period,
255
- "aria-haspopup": "menu",
256
- "aria-expanded": periodOpen,
257
- onClick: () => setPeriodOpen((open) => !open),
258
- children: [periods.find(({ id }) => id === String(period)).label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})]
259
- })
339
+ onChange: (id) => setPeriod(Number(id))
260
340
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
261
341
  variant: "outline",
262
342
  className: "dsh-usage-refresh",
263
343
  "aria-busy": busy,
264
344
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
265
345
  disabled: busy,
266
- onClick: () => setRefresh((value) => value + 1),
346
+ onClick: () => {
347
+ refreshRequested.current = true;
348
+ setRefresh((value) => value + 1);
349
+ },
267
350
  children: busy ? text.refreshing : text.refresh
268
351
  })]
269
352
  }),
@@ -281,8 +364,12 @@ function Usage({ ctx }) {
281
364
  className: "dsh-usage-summary",
282
365
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text.total }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
283
366
  className: "dsh-usage-total",
284
- children: number.format(view.totals.total)
285
- })] }), metrics.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: number.format(view.totals[key]) })] }, key))]
367
+ title: number.format(view.totals.total),
368
+ children: tokens.format(view.totals.total)
369
+ })] }), metrics.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
370
+ title: number.format(view.totals[key]),
371
+ children: tokens.format(view.totals[key])
372
+ })] }, key))]
286
373
  }),
287
374
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
288
375
  className: "dsh-usage-meta dsh-usage-muted",
@@ -337,7 +424,62 @@ function Usage({ ctx }) {
337
424
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
338
425
  className: "dsh-usage-detail",
339
426
  "aria-live": "polite",
340
- children: selected && `${date(selected.day)} · ${text.total}: ${number.format(selected.total)}`
427
+ title: selected && number.format(selected.total),
428
+ children: selected && `${date(selected.day)} · ${text.total}: ${tokens.format(selected.total)}`
429
+ }),
430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.requests }),
431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
432
+ className: "dsh-usage-muted",
433
+ children: text.requestScope
434
+ }),
435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
436
+ className: "dsh-usage-calls",
437
+ tabIndex: 0,
438
+ "aria-label": text.requests,
439
+ children: view.calls.map((call) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
440
+ className: "dsh-usage-model",
441
+ children: [
442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
443
+ className: "dsh-usage-call-meta",
444
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("time", {
445
+ className: "dsh-usage-muted",
446
+ dateTime: new Date(call.time).toISOString(),
447
+ children: [
448
+ text.time,
449
+ ":",
450
+ " ",
451
+ new Intl.DateTimeFormat(locale.active, {
452
+ dateStyle: "medium",
453
+ timeStyle: "medium",
454
+ timeZone: view.timeZone
455
+ }).format(call.time)
456
+ ]
457
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
458
+ className: "dsh-usage-call-status",
459
+ "data-status": call.status,
460
+ children: [
461
+ text.status,
462
+ ": ",
463
+ text[call.status]
464
+ ]
465
+ })]
466
+ }),
467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
468
+ call.provider || text.unknown,
469
+ " / ",
470
+ call.model || text.unknown
471
+ ] }),
472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
473
+ className: "dsh-usage-muted",
474
+ children: [
475
+ text.session,
476
+ ": ",
477
+ call.session
478
+ ]
479
+ }),
480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["total", ...metrics].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: call.unreported ? text.notReported : number.format(call[key]) })] }, key)) })
481
+ ]
482
+ }, JSON.stringify([call.session, call.seq])))
341
483
  }),
342
484
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.models }),
343
485
  view.models.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -348,8 +490,14 @@ function Usage({ ctx }) {
348
490
  row.provider || text.unknown,
349
491
  " / ",
350
492
  row.model || text.unknown
351
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: number.format(row.total) })]
352
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["calls", ...metrics].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: number.format(row[key]) })] }, key)) })]
493
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
494
+ title: number.format(row.total),
495
+ children: tokens.format(row.total)
496
+ })]
497
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["calls", ...metrics].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: text[key] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
498
+ title: number.format(row[key]),
499
+ children: key === "calls" ? number.format(row[key]) : tokens.format(row[key])
500
+ })] }, key)) })]
353
501
  }, JSON.stringify([row.provider, row.model])))
354
502
  ] }),
355
503
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
@@ -387,10 +535,7 @@ function apply(ctx) {
387
535
  name: "main",
388
536
  key: name,
389
537
  inject: () => ({ ctx })
390
- }, ({ ctx }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
391
- className: "dsh-usage-page",
392
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Usage, { ctx })
393
- })));
538
+ }, ({ ctx }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginPage, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Usage, { ctx }) })));
394
539
  ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
395
540
  name: "sidebar.panellist",
396
541
  id: name,
package/dist/index.js CHANGED
@@ -77,6 +77,7 @@ function apply(ctx) {
77
77
  const view = {
78
78
  totals: empty(),
79
79
  models: [],
80
+ calls: [],
80
81
  days: [],
81
82
  sessions: 0,
82
83
  unreadable: 0,
@@ -121,6 +122,20 @@ function apply(ctx) {
121
122
  const before = row.total;
122
123
  add(row, usage);
123
124
  add(view.totals, usage);
125
+ const finish = lastAssistantStreamChunk(event.data.stream, "finish")?.reason.kind;
126
+ const call = {
127
+ ...empty(),
128
+ session: record.header.id,
129
+ seq: event.seq,
130
+ time: event.time,
131
+ provider: route.provider,
132
+ model: route.model,
133
+ status: finish === "error" ? "failure" : finish === "aborted" || event.type === "assistant/message" && event.data.interrupted ? "interrupted" : event.type === "assistant/message" ? "success" : "unknownStatus"
134
+ };
135
+ add(call, usage);
136
+ view.calls.push(call);
137
+ view.calls.sort((a, b) => b.time - a.time || a.session.localeCompare(b.session) || b.seq - a.seq);
138
+ if (view.calls.length > 100) view.calls.pop();
124
139
  days.set(day, (days.get(day) ?? 0) + row.total - before);
125
140
  included = true;
126
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-usage",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Recorded session token usage dashboard for DeepSeek Harness.",
5
5
  "license": "MIT",
6
6
  "repository": {