@dungsil/dsh-plugin-ui-usage 0.3.0 → 0.4.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
@@ -71,6 +71,13 @@ const copy = {
71
71
  week: "Last 7 days",
72
72
  month: "Last 30 days",
73
73
  all: "All time",
74
+ custom: "Custom range",
75
+ startDate: "Start date",
76
+ endDate: "End date",
77
+ apply: "Apply",
78
+ showing: "Showing",
79
+ rangeHint: "Includes both dates, up to 366 days. Uses your browser time zone.",
80
+ rangeError: "Enter valid dates in order, no more than 366 days apart including both dates.",
74
81
  refresh: "Refresh",
75
82
  refreshing: "Refreshing…",
76
83
  loading: "Loading usage…",
@@ -109,6 +116,13 @@ const copy = {
109
116
  week: "최근 7일",
110
117
  month: "최근 30일",
111
118
  all: "전체 기간",
119
+ custom: "사용자 지정",
120
+ startDate: "시작일",
121
+ endDate: "종료일",
122
+ apply: "적용",
123
+ showing: "조회 중인 기간",
124
+ rangeHint: "시작일과 종료일을 포함해 최대 366일까지 조회합니다. 브라우저 시간대를 사용합니다.",
125
+ rangeError: "시작일과 종료일을 올바르게 입력해 주세요. 종료일은 시작일과 같거나 늦어야 하며, 두 날짜를 포함해 366일 이내여야 합니다.",
112
126
  refresh: "새로고침",
113
127
  refreshing: "새로고침 중…",
114
128
  loading: "사용량을 불러오는 중입니다…",
@@ -147,6 +161,13 @@ const copy = {
147
161
  week: "最近 7 天",
148
162
  month: "最近 30 天",
149
163
  all: "全部时间",
164
+ custom: "自定义范围",
165
+ startDate: "开始日期",
166
+ endDate: "结束日期",
167
+ apply: "应用",
168
+ showing: "当前查询范围",
169
+ rangeHint: "包含起止日期,最多 366 天。使用浏览器时区。",
170
+ rangeError: "请输入有效且顺序正确的日期,包含起止日期的范围不得超过 366 天。",
150
171
  refresh: "刷新",
151
172
  refreshing: "正在刷新…",
152
173
  loading: "正在加载用量…",
@@ -184,6 +205,16 @@ const localizedCopy = (language) => copy[language.startsWith("ko") ? "ko" : lang
184
205
  // src/protocol.ts
185
206
  const channel = "/api";
186
207
  const method = "dungsil-usage/describe";
208
+ function isValidDateRange(startDate, endDate) {
209
+ const parse = (value) => {
210
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return NaN;
211
+ const time = Date.parse(`${value}T00:00:00Z`);
212
+ return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value ? time : NaN;
213
+ };
214
+ const start = parse(startDate);
215
+ const inclusiveDays = (parse(endDate) - start) / 864e5 + 1;
216
+ return inclusiveDays >= 1 && inclusiveDays <= 366;
217
+ }
187
218
  // src/client.tsx
188
219
  const name = "dungsil-ui-usage";
189
220
  const inject = [
@@ -192,10 +223,20 @@ const inject = [
192
223
  "slots"
193
224
  ];
194
225
  const cachedViews = /* @__PURE__ */ new WeakMap();
226
+ const savedPeriods = /* @__PURE__ */ new WeakMap();
227
+ function localDay(offset = 0) {
228
+ const date = /* @__PURE__ */ new Date();
229
+ date.setDate(date.getDate() + offset);
230
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
231
+ }
195
232
  const styles = `${pageStyles}
196
233
  .dsh-usage { container-type: inline-size; overflow-wrap: anywhere; color: var(--dsw-alias-label-primary); font: 14px/22px var(--dsw-font-family, system-ui); }
197
234
  .dsh-usage-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
198
235
  .dsh-usage-status { display: flex; align-items: center; gap: 6px; }
236
+ .dsh-usage-range { display: flex; align-items: end; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
237
+ .dsh-usage-range label { display: flex; flex: 1 1 160px; min-width: 0; flex-direction: column; gap: 6px; }
238
+ .dsh-usage-range input { width: 100%; min-width: 0; box-sizing: border-box; color-scheme: light dark; }
239
+ .dsh-usage-range-note { flex-basis: 100%; margin: 0; }
199
240
  @keyframes dsh-usage-spin { to { transform: rotate(360deg); } }
200
241
  @media (prefers-reduced-motion: no-preference) {
201
242
  .dsh-usage-refresh[aria-busy=true] svg { animation: dsh-usage-spin .8s linear infinite; }
@@ -245,10 +286,19 @@ function Usage({ ctx }) {
245
286
  notation: "compact",
246
287
  maximumFractionDigits: 1
247
288
  });
248
- const [period, setPeriod] = (0, react.useState)(7);
289
+ const connection = ctx.get("connection");
290
+ const [period, setPeriod] = (0, react.useState)(() => savedPeriods.get(connection) ?? { days: 7 });
291
+ const [editingRange, setEditingRange] = (0, react.useState)(() => !("days" in period));
292
+ const [startDate, setStartDate] = (0, react.useState)(() => "startDate" in period ? period.startDate : localDay(-6));
293
+ const [endDate, setEndDate] = (0, react.useState)(() => "endDate" in period ? period.endDate : localDay());
294
+ const [rangeError, setRangeError] = (0, react.useState)(false);
295
+ const rangeId = (0, react.useId)();
296
+ const applyPeriod = (next) => {
297
+ savedPeriods.set(connection, next);
298
+ setPeriod(next);
299
+ };
249
300
  const [refresh, setRefresh] = (0, react.useState)(0);
250
301
  const refreshRequested = (0, react.useRef)(false);
251
- const connection = ctx.get("connection");
252
302
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
253
303
  const cacheKey = JSON.stringify([period, timeZone]);
254
304
  let cache = cachedViews.get(connection);
@@ -272,7 +322,7 @@ function Usage({ ctx }) {
272
322
  (async () => {
273
323
  try {
274
324
  const result = await connection.rpc.call(channel, method, {
275
- days: period,
325
+ ...period,
276
326
  timeZone
277
327
  }, controller.signal);
278
328
  if (controller.signal.aborted) return;
@@ -314,6 +364,10 @@ function Usage({ ctx }) {
314
364
  {
315
365
  id: "0",
316
366
  label: text.all
367
+ },
368
+ {
369
+ id: "custom",
370
+ label: text.custom
317
371
  }
318
372
  ];
319
373
  const selected = view?.days.find(({ day }) => day === selectedDay);
@@ -334,9 +388,13 @@ function Usage({ ctx }) {
334
388
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonPicker, {
335
389
  variant: "outline",
336
390
  label: text.period,
337
- value: String(period),
391
+ value: editingRange || !("days" in period) ? "custom" : String(period.days),
338
392
  items: periods,
339
- onChange: (id) => setPeriod(Number(id))
393
+ onChange: (id) => {
394
+ setEditingRange(id === "custom");
395
+ setRangeError(false);
396
+ if (id !== "custom") applyPeriod({ days: Number(id) });
397
+ }
340
398
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
341
399
  variant: "outline",
342
400
  className: "dsh-usage-refresh",
@@ -350,6 +408,76 @@ function Usage({ ctx }) {
350
408
  children: busy ? text.refreshing : text.refresh
351
409
  })]
352
410
  }),
411
+ editingRange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
412
+ className: "dsh-usage-range",
413
+ noValidate: true,
414
+ onSubmit: (event) => {
415
+ event.preventDefault();
416
+ if (!isValidDateRange(startDate, endDate)) {
417
+ setRangeError(true);
418
+ return;
419
+ }
420
+ setRangeError(false);
421
+ applyPeriod({
422
+ startDate,
423
+ endDate
424
+ });
425
+ },
426
+ children: [
427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.startDate, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
428
+ type: "date",
429
+ value: startDate,
430
+ required: true,
431
+ "aria-invalid": rangeError,
432
+ "aria-describedby": `${rangeId}-hint${rangeError ? ` ${rangeId}-error` : ""}`,
433
+ onChange: (event) => {
434
+ setStartDate(event.target.value);
435
+ setRangeError(false);
436
+ }
437
+ })] }),
438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.endDate, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
439
+ type: "date",
440
+ value: endDate,
441
+ required: true,
442
+ "aria-invalid": rangeError,
443
+ "aria-describedby": `${rangeId}-hint${rangeError ? ` ${rangeId}-error` : ""}`,
444
+ onChange: (event) => {
445
+ setEndDate(event.target.value);
446
+ setRangeError(false);
447
+ }
448
+ })] }),
449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
450
+ type: "submit",
451
+ variant: "outline",
452
+ children: text.apply
453
+ }),
454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
455
+ id: `${rangeId}-hint`,
456
+ className: "dsh-usage-muted dsh-usage-range-note",
457
+ children: [
458
+ text.rangeHint,
459
+ " (",
460
+ timeZone,
461
+ ")"
462
+ ]
463
+ }),
464
+ rangeError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
465
+ id: `${rangeId}-error`,
466
+ className: "dsh-usage-range-note",
467
+ role: "alert",
468
+ children: text.rangeError
469
+ })
470
+ ]
471
+ }),
472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
473
+ className: "dsh-usage-muted",
474
+ children: [
475
+ text.showing,
476
+ ":",
477
+ " ",
478
+ "days" in period ? periods.find(({ id }) => id === String(period.days))?.label : `${period.startDate} – ${period.endDate}`
479
+ ]
480
+ }),
353
481
  busy && !view && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
354
482
  role: "status",
355
483
  className: "dsh-usage-status",
@@ -403,7 +531,7 @@ function Usage({ ctx }) {
403
531
  role: "status",
404
532
  children: text.empty
405
533
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
406
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: period === 0 ? text.recent : text.daily }),
534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "days" in period && period.days === 0 ? text.recent : text.daily }),
407
535
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
408
536
  className: "dsh-usage-chart",
409
537
  role: "group",
package/dist/index.js CHANGED
@@ -2,6 +2,16 @@ import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
2
2
  import { lastAssistantStreamChunk } from "@deepseek-ai/dsh-llm";
3
3
  // src/protocol.ts
4
4
  const method = "dungsil-usage/describe";
5
+ function isValidDateRange(startDate, endDate) {
6
+ const parse = (value) => {
7
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return NaN;
8
+ const time = Date.parse(`${value}T00:00:00Z`);
9
+ return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value ? time : NaN;
10
+ };
11
+ const start = parse(startDate);
12
+ const inclusiveDays = (parse(endDate) - start) / 864e5 + 1;
13
+ return inclusiveDays >= 1 && inclusiveDays <= 366;
14
+ }
5
15
  // src/index.ts
6
16
  const name = "dungsil-ui-usage";
7
17
  const inject = ["connection", "sessionQuery"];
@@ -46,11 +56,11 @@ function apply(ctx) {
46
56
  const body = clientRequestSchema.parse(await request.json());
47
57
  rpcId = body.rpcId;
48
58
  const input = body.payload;
49
- if (body.method !== "dungsil-usage/describe" || !input || ![
59
+ if (body.method !== "dungsil-usage/describe" || !input || typeof input !== "object" || Array.isArray(input) || typeof input.timeZone !== "string" || ("startDate" in input || "endDate" in input ? "days" in input || typeof input.startDate !== "string" || typeof input.endDate !== "string" || !isValidDateRange(input.startDate, input.endDate) : ![
50
60
  7,
51
61
  30,
52
62
  0
53
- ].includes(input.days) || typeof input.timeZone !== "string") return Response.json({
63
+ ].includes(input.days))) return Response.json({
54
64
  type: "server-response",
55
65
  rpcId,
56
66
  result: failure("invalid")
@@ -73,7 +83,8 @@ function apply(ctx) {
73
83
  const signal = AbortSignal.any([request.signal, AbortSignal.timeout(6e4)]);
74
84
  const updatedAt = Date.now();
75
85
  const today = formatter.format(updatedAt);
76
- const start = input.days === 0 ? "" : (/* @__PURE__ */ new Date(Date.parse(`${today}T00:00:00Z`) - (input.days - 1) * 864e5)).toISOString().slice(0, 10);
86
+ const end = typeof input.endDate === "string" ? input.endDate : today;
87
+ const start = typeof input.startDate === "string" ? input.startDate : input.days === 0 ? "" : (/* @__PURE__ */ new Date(Date.parse(`${today}T00:00:00Z`) - (input.days - 1) * 864e5)).toISOString().slice(0, 10);
77
88
  const view = {
78
89
  totals: empty(),
79
90
  models: [],
@@ -106,7 +117,7 @@ function apply(ctx) {
106
117
  if (event.seq < observation.inheritedEventCount || event.time > updatedAt) continue;
107
118
  if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
108
119
  const day = formatter.format(event.time);
109
- if (day < start || day > today) continue;
120
+ if (day < start || day > end) continue;
110
121
  const usage = event.type === "assistant/message" ? event.data.usage : lastAssistantStreamChunk(event.data.stream, "usage")?.usage;
111
122
  const route = event.type === "assistant/message" ? event.data.message.source : {
112
123
  provider,
@@ -148,7 +159,7 @@ function apply(ctx) {
148
159
  }));
149
160
  view.models = [...models.values()].sort((a, b) => b.total - a.total || a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model));
150
161
  const chartStart = start || (/* @__PURE__ */ new Date(Date.parse(`${today}T00:00:00Z`) - 25056e5)).toISOString().slice(0, 10);
151
- for (let date = Date.parse(`${chartStart}T00:00:00Z`); date <= Date.parse(`${today}T00:00:00Z`); date += 864e5) {
162
+ for (let date = Date.parse(`${chartStart}T00:00:00Z`); date <= Date.parse(`${end}T00:00:00Z`); date += 864e5) {
152
163
  const day = new Date(date).toISOString().slice(0, 10);
153
164
  view.days.push({
154
165
  day,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-usage",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Recorded session token usage dashboard for DeepSeek Harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,15 +25,15 @@
25
25
  "access": "public"
26
26
  },
27
27
  "devDependencies": {
28
- "@deepseek-ai/cordis": "4.0.5",
29
- "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
30
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
31
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.5",
32
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
33
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.5",
34
- "@deepseek-ai/dsh-credentials-local": "0.2.1-rc.5",
35
- "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
36
- "@deepseek-ai/dsh-session-query": "0.2.1-rc.5",
28
+ "@deepseek-ai/cordis": "4.1.0",
29
+ "@deepseek-ai/dsh-client-connection": "0.2.2-rc.1",
30
+ "@deepseek-ai/dsh-client-locale": "0.2.2-rc.1",
31
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.2-rc.1",
32
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.2-rc.1",
33
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.2-rc.1",
34
+ "@deepseek-ai/dsh-credentials-local": "0.2.2-rc.1",
35
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
36
+ "@deepseek-ai/dsh-session-query": "0.2.2-rc.1",
37
37
  "@dungsil/dsh-tsconfig": "0.0.0",
38
38
  "@dungsil/dsh-tsdown": "0.0.0",
39
39
  "@dungsil/dsh-ui": "0.0.0",
@@ -44,13 +44,13 @@
44
44
  "tsdown": "0.23.4"
45
45
  },
46
46
  "peerDependencies": {
47
- "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.5",
48
- "@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.5",
49
- "@deepseek-ai/dsh-llm": "^0.2.1-rc.5",
50
- "@deepseek-ai/dsh-session-query": "^0.2.1-rc.5"
47
+ "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.5 || ^0.2.2-rc.1",
48
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.5 || ^0.2.2-rc.1",
49
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.5 || ^0.2.2-rc.1",
50
+ "@deepseek-ai/dsh-session-query": "^0.2.1-rc.5 || ^0.2.2-rc.1"
51
51
  },
52
52
  "engines": {
53
- "dsh": "^0.2.1-rc.5",
53
+ "dsh": "^0.2.1-rc.5 || ^0.2.2-rc.1",
54
54
  "node": ">=24.9.0"
55
55
  },
56
56
  "dsh": {