@mingozhou/dsh-replay 0.4.1 → 0.4.2

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 (53) hide show
  1. package/README.md +225 -148
  2. package/README.zh.md +225 -154
  3. package/assets/icon.png +0 -0
  4. package/lib/client.js +954 -2418
  5. package/lib/core.js +557 -86
  6. package/lib/index.js +1043 -51
  7. package/lib/types/src/client/api.d.ts +44 -0
  8. package/lib/types/src/client/components/AuditView.d.ts +2 -1
  9. package/lib/types/src/client/components/CompareView.d.ts +2 -1
  10. package/lib/types/src/client/components/DashboardView.d.ts +8 -0
  11. package/lib/types/src/client/components/ExportDialog.d.ts +9 -0
  12. package/lib/types/src/client/components/ReplayApp.d.ts +4 -2
  13. package/lib/types/src/client/components/ReplayModal.d.ts +1 -1
  14. package/lib/types/src/client/components/SessionPicker.d.ts +4 -3
  15. package/lib/types/src/client/components/TimelineView.d.ts +2 -1
  16. package/lib/types/src/client/export.d.ts +6 -1
  17. package/lib/types/src/client/i18n.d.ts +8 -1
  18. package/lib/types/src/client/pricing.d.ts +6 -0
  19. package/lib/types/src/core/audit.d.ts +18 -0
  20. package/lib/types/src/core/compare.d.ts +8 -3
  21. package/lib/types/src/core/cost.d.ts +21 -3
  22. package/lib/types/src/core/index.d.ts +1 -0
  23. package/lib/types/src/core/redact.d.ts +40 -0
  24. package/lib/types/src/core/timeline.d.ts +9 -0
  25. package/lib/types/src/index.d.ts +52 -1
  26. package/lib/viewer.js +282 -29
  27. package/package.json +118 -107
  28. package/scripts/build.mjs +7 -1
  29. package/src/client/api.ts +114 -10
  30. package/src/client/components/AuditView.tsx +89 -10
  31. package/src/client/components/CompareView.tsx +89 -13
  32. package/src/client/components/DashboardView.tsx +232 -0
  33. package/src/client/components/ExportDialog.tsx +136 -0
  34. package/src/client/components/ItemDetail.tsx +36 -4
  35. package/src/client/components/OverviewView.tsx +150 -34
  36. package/src/client/components/ReplayApp.tsx +207 -60
  37. package/src/client/components/ReplayModal.tsx +41 -6
  38. package/src/client/components/SessionPicker.tsx +55 -27
  39. package/src/client/components/TimelineView.tsx +106 -41
  40. package/src/client/export.ts +20 -4
  41. package/src/client/i18n.ts +266 -45
  42. package/src/client/index.ts +7 -5
  43. package/src/client/mascot-image.ts +1 -1
  44. package/src/client/pricing.ts +90 -0
  45. package/src/client/styles.css +250 -1
  46. package/src/client/viewer-entry.tsx +25 -4
  47. package/src/core/audit.ts +274 -67
  48. package/src/core/compare.ts +0 -0
  49. package/src/core/cost.ts +94 -16
  50. package/src/core/index.ts +1 -0
  51. package/src/core/redact.ts +182 -0
  52. package/src/core/timeline.ts +107 -12
  53. package/src/index.ts +290 -54
@@ -0,0 +1,90 @@
1
+ /**
2
+ * User-editable pricing overrides. Persisted in localStorage; the default
3
+ * table still lives in core/cost.ts so the demo and CLI tools work without UI.
4
+ */
5
+ import React from 'react'
6
+ import { DEFAULT_PRICING, type PricingEntry, type PricingRate } from '../core/cost.js'
7
+
8
+ const KEY = 'dshr-pricing-overrides'
9
+
10
+ type Overrides = Record<string, PricingRate>
11
+
12
+ function read(): Overrides {
13
+ try {
14
+ const raw = globalThis.localStorage?.getItem(KEY)
15
+ if (raw === null || raw === undefined) return {}
16
+ const parsed = JSON.parse(raw) as unknown
17
+ if (typeof parsed !== 'object' || parsed === null) return {}
18
+ const out: Overrides = {}
19
+ for (const [label, value] of Object.entries(parsed)) {
20
+ if (typeof value !== 'object' || value === null) continue
21
+ const rate = value as Record<string, unknown>
22
+ if (
23
+ typeof rate['input'] === 'number' &&
24
+ typeof rate['cacheRead'] === 'number' &&
25
+ typeof rate['output'] === 'number'
26
+ ) {
27
+ out[label] = {
28
+ input: rate['input'],
29
+ cacheRead: rate['cacheRead'],
30
+ output: rate['output'],
31
+ ...(typeof rate['cacheWrite'] === 'number' ? { cacheWrite: rate['cacheWrite'] } : {}),
32
+ }
33
+ }
34
+ }
35
+ return out
36
+ } catch {
37
+ return {}
38
+ }
39
+ }
40
+
41
+ let overrides: Overrides = read()
42
+ let cached: PricingEntry[] = build(overrides)
43
+ const listeners = new Set<() => void>()
44
+
45
+ function build(source: Overrides): PricingEntry[] {
46
+ return DEFAULT_PRICING.map(entry =>
47
+ source[entry.label] === undefined
48
+ ? entry
49
+ : { ...entry, rate: source[entry.label], source: 'user override' },
50
+ )
51
+ }
52
+
53
+ function snapshot(): PricingEntry[] {
54
+ return cached
55
+ }
56
+
57
+ export function getPricing(): PricingEntry[] {
58
+ return cached
59
+ }
60
+
61
+ export function setRate(label: string, rate: PricingRate): void {
62
+ overrides = { ...overrides, [label]: rate }
63
+ cached = build(overrides)
64
+ try {
65
+ globalThis.localStorage?.setItem(KEY, JSON.stringify(overrides))
66
+ } catch {
67
+ /* best effort */
68
+ }
69
+ for (const listener of listeners) listener()
70
+ }
71
+
72
+ export function resetPricing(): void {
73
+ overrides = {}
74
+ cached = build(overrides)
75
+ try {
76
+ globalThis.localStorage?.removeItem(KEY)
77
+ } catch {
78
+ /* best effort */
79
+ }
80
+ for (const listener of listeners) listener()
81
+ }
82
+
83
+ export function subscribePricing(listener: () => void): () => void {
84
+ listeners.add(listener)
85
+ return () => listeners.delete(listener)
86
+ }
87
+
88
+ export function usePricing(): PricingEntry[] {
89
+ return React.useSyncExternalStore(subscribePricing, snapshot, snapshot)
90
+ }
@@ -53,7 +53,9 @@ body[data-ds-dark-theme] .dshr-root,
53
53
  .dshr-root *::before,
54
54
  .dshr-root *::after { box-sizing: border-box; }
55
55
 
56
- .dshr-root button {
56
+ /* Keep the button reset weak enough that component classes can style a button.
57
+ The previous `.dshr-root button` selector beat `.dshr-btn` / card classes. */
58
+ .dshr-root :where(button) {
57
59
  font: inherit;
58
60
  color: inherit;
59
61
  background: none;
@@ -717,3 +719,250 @@ body[data-ds-dark-theme] .dshr-root,
717
719
  margin-bottom: 8px;
718
720
  }
719
721
  .dshr-root .dshr-picker-row:hover { border-color: var(--dshr-accent); }
722
+
723
+ /* ---- 0.4.2: archive dashboard, cost editor, export dialog, audit ---- */
724
+ .dshr-header-sub {
725
+ color: var(--dshr-ink-3);
726
+ font-size: 11px;
727
+ white-space: nowrap;
728
+ }
729
+ .dshr-header-session {
730
+ max-width: 48vw;
731
+ overflow: hidden;
732
+ text-overflow: ellipsis;
733
+ white-space: nowrap;
734
+ }
735
+ .dshr-live-dot {
736
+ width: 7px;
737
+ height: 7px;
738
+ border-radius: 50%;
739
+ background: var(--dshr-good);
740
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dshr-good) 18%, transparent);
741
+ flex: none;
742
+ }
743
+ .dshr-live-pill {
744
+ font-size: 10px;
745
+ padding: 0 7px;
746
+ border-radius: 999px;
747
+ color: #fff;
748
+ background: var(--dshr-good);
749
+ letter-spacing: 0.02em;
750
+ }
751
+ .dshr-error-state .dshr-btn { margin-top: 8px; }
752
+
753
+ .dshr-dashboard {
754
+ flex: 1;
755
+ min-height: 0;
756
+ overflow-y: auto;
757
+ padding: 18px 20px 28px;
758
+ display: flex;
759
+ flex-direction: column;
760
+ gap: 14px;
761
+ }
762
+ .dshr-dash-head { display: flex; align-items: flex-end; gap: 12px; }
763
+ .dshr-dash-subtitle { font-size: 15px; font-weight: 600; }
764
+ .dshr-dash-scanned { margin-top: 2px; color: var(--dshr-ink-3); font-size: 11px; }
765
+ .dshr-dash-cards {
766
+ display: grid;
767
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
768
+ gap: 10px;
769
+ }
770
+ .dshr-dash-card {
771
+ text-align: left;
772
+ padding: 14px 16px;
773
+ border: 1px solid var(--dshr-border);
774
+ border-radius: 12px;
775
+ background: var(--dshr-bg-1);
776
+ min-width: 0;
777
+ }
778
+ .dshr-dash-card-action { cursor: pointer; }
779
+ .dshr-dash-card-action:hover { border-color: var(--dshr-accent); }
780
+ .dshr-dash-risk { border-color: color-mix(in srgb, var(--dshr-critical) 45%, var(--dshr-border)); }
781
+ .dshr-dash-value {
782
+ font-size: 26px;
783
+ font-weight: 650;
784
+ font-variant-numeric: tabular-nums;
785
+ letter-spacing: -0.02em;
786
+ overflow: hidden;
787
+ text-overflow: ellipsis;
788
+ white-space: nowrap;
789
+ }
790
+ .dshr-dash-label { margin-top: 2px; color: var(--dshr-ink-3); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
791
+ .dshr-dash-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
792
+ .dshr-dash-search { width: min(320px, 46vw); }
793
+ .dshr-dash-count { color: var(--dshr-ink-3); font-size: 11px; }
794
+ .dshr-dash-list { display: flex; flex-direction: column; gap: 6px; }
795
+ .dshr-dash-row {
796
+ display: flex;
797
+ align-items: center;
798
+ gap: 14px;
799
+ width: 100%;
800
+ text-align: left;
801
+ padding: 10px 12px;
802
+ border: 1px solid var(--dshr-border);
803
+ border-radius: 10px;
804
+ background: var(--dshr-bg-1);
805
+ }
806
+ .dshr-dash-row:hover { border-color: var(--dshr-accent); background: var(--dshr-bg-2); }
807
+ .dshr-dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
808
+ .dshr-dash-title { display: flex; align-items: center; gap: 7px; font-weight: 550; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
809
+ .dshr-dash-sub { color: var(--dshr-ink-3); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
810
+ .dshr-dash-metrics { display: flex; align-items: center; gap: 10px; flex: none; color: var(--dshr-ink-2); font-size: 11px; font-variant-numeric: tabular-nums; }
811
+ .dshr-dash-duration { color: var(--dshr-ink-3); min-width: 46px; text-align: right; }
812
+ .dshr-dash-pending { color: var(--dshr-ink-3); }
813
+ .dshr-risk-badge { color: var(--dshr-critical); font-weight: 600; }
814
+ .dshr-ok-badge { color: var(--dshr-good); }
815
+ .dshr-skeleton {
816
+ min-height: 64px;
817
+ border-radius: 10px;
818
+ background: linear-gradient(90deg, var(--dshr-bg-1), var(--dshr-bg-2), var(--dshr-bg-1));
819
+ background-size: 200% 100%;
820
+ animation: dshr-shimmer 1.3s ease-in-out infinite;
821
+ }
822
+ @keyframes dshr-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
823
+ .dshr-segmented {
824
+ display: inline-flex;
825
+ padding: 2px;
826
+ border: 1px solid var(--dshr-border);
827
+ border-radius: 999px;
828
+ background: var(--dshr-bg-1);
829
+ }
830
+ .dshr-segmented button {
831
+ font-size: 11px;
832
+ padding: 2px 9px;
833
+ border-radius: 999px;
834
+ color: var(--dshr-ink-3);
835
+ }
836
+ .dshr-segmented button[data-active] { background: var(--dshr-bg); color: var(--dshr-ink); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
837
+
838
+ .dshr-picker-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
839
+ .dshr-picker-search { width: min(320px, 46vw); }
840
+ .dshr-picker-count { color: var(--dshr-ink-3); font-size: 11px; }
841
+
842
+ .dshr-cost-panel { display: flex; flex-direction: column; gap: 6px; }
843
+ .dshr-cost-total { display: flex; align-items: baseline; gap: 12px; font-size: 13px; }
844
+ .dshr-cost-total strong { font-size: 22px; font-variant-numeric: tabular-nums; }
845
+ .dshr-cost-table { margin-top: 4px; max-width: 540px; }
846
+ .dshr-cost-note { margin: 0; color: var(--dshr-ink-3); font-size: 11px; }
847
+ .dshr-price-editor { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--dshr-border); }
848
+ .dshr-price-table { max-width: 560px; }
849
+ .dshr-price-table td:not(:first-child), .dshr-price-table th:not(:first-child) { text-align: right; }
850
+ .dshr-price-input {
851
+ width: 78px;
852
+ font: inherit;
853
+ font-size: 12px;
854
+ text-align: right;
855
+ color: var(--dshr-ink);
856
+ background: var(--dshr-bg);
857
+ border: 1px solid var(--dshr-border);
858
+ border-radius: 6px;
859
+ padding: 3px 6px;
860
+ }
861
+ .dshr-price-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
862
+ .dshr-btn {
863
+ font: inherit;
864
+ font-size: 12px;
865
+ cursor: pointer;
866
+ padding: 5px 12px;
867
+ border-radius: 7px;
868
+ border: 1px solid var(--dshr-border);
869
+ background: var(--dshr-bg-1);
870
+ color: var(--dshr-ink);
871
+ }
872
+ .dshr-btn:hover { border-color: var(--dshr-accent); }
873
+ .dshr-btn:disabled { opacity: 0.5; cursor: default; }
874
+ .dshr-btn-primary { background: var(--dshr-accent); border-color: var(--dshr-accent); color: #fff; }
875
+ .dshr-btn-primary:hover { filter: brightness(1.06); }
876
+
877
+ .dshr-copy-btn {
878
+ margin-left: 8px;
879
+ font-size: 10px;
880
+ font-weight: 400;
881
+ text-transform: none;
882
+ letter-spacing: 0;
883
+ color: var(--dshr-ink-3);
884
+ border: 1px solid var(--dshr-border);
885
+ border-radius: 5px;
886
+ padding: 1px 6px;
887
+ }
888
+ .dshr-copy-btn:hover { color: var(--dshr-ink); border-color: var(--dshr-accent); }
889
+ .dshr-reasoning {
890
+ margin: 0 0 10px;
891
+ border: 1px solid var(--dshr-border);
892
+ border-radius: 8px;
893
+ background: var(--dshr-bg-1);
894
+ padding: 7px 10px;
895
+ }
896
+ .dshr-reasoning summary { cursor: pointer; color: var(--dshr-ink-2); font-size: 11px; font-weight: 600; }
897
+ .dshr-reasoning .dshr-pre { margin: 7px 0 0; border: 0; background: transparent; padding: 0; max-height: 34vh; }
898
+ .dshr-item-thinking { flex: none; color: var(--dshr-accent); font-size: 10px; }
899
+ .dshr-rail:focus-visible { outline: 2px solid var(--dshr-accent); outline-offset: -2px; }
900
+
901
+ .dshr-audit-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
902
+ .dshr-audit-coverage { color: var(--dshr-ink-3); font-size: 11px; }
903
+ .dshr-audit-note { margin: -4px 0 12px; color: var(--dshr-ink-3); font-size: 11px; line-height: 1.5; }
904
+ .dshr-audit-tile { cursor: pointer; background: transparent; text-align: left; }
905
+ .dshr-audit-tile[data-active] { border-color: var(--dshr-accent); box-shadow: 0 0 0 1px var(--dshr-accent); }
906
+ .dshr-finding-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
907
+ .dshr-category {
908
+ margin-left: 8px;
909
+ font-size: 9px;
910
+ padding: 0 6px;
911
+ border-radius: 999px;
912
+ color: var(--dshr-ink-3);
913
+ background: var(--dshr-bg-2);
914
+ text-transform: uppercase;
915
+ letter-spacing: 0.04em;
916
+ }
917
+ .dshr-divergence-title { font-size: 12px; margin-bottom: 8px; }
918
+ .dshr-diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
919
+ .dshr-diff-side .dshr-pre { max-height: 220px; }
920
+ .dshr-diff-side .dshr-tab { margin-top: 2px; }
921
+
922
+ .dshr-export-backdrop { z-index: 1100; }
923
+ .dshr-export {
924
+ width: min(560px, 92vw);
925
+ height: auto;
926
+ max-height: 86vh;
927
+ }
928
+ .dshr-export-body { padding: 14px 18px 16px; display: flex; flex-direction: column; gap: 9px; overflow-y: auto; }
929
+ .dshr-export-warning { margin: 0; color: var(--dshr-ink-2); font-size: 12px; line-height: 1.55; }
930
+ .dshr-check { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; }
931
+ .dshr-check input { margin-top: 2px; accent-color: var(--dshr-accent); }
932
+ .dshr-export-meta { margin: 0; color: var(--dshr-ink-3); font-size: 11px; }
933
+ .dshr-export-danger { margin: 0; color: var(--dshr-critical); font-size: 12px; }
934
+ .dshr-export-note { margin: 0; color: var(--dshr-ink-3); font-size: 11px; line-height: 1.5; }
935
+ .dshr-export-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
936
+
937
+ /* contrast: 11px tertiary text needs a darker shade than #898781 on #fcfcfb */
938
+ .dshr-root { --dshr-ink-3: var(--dsw-alias-label-tertiary, #6f6e69); }
939
+ body[data-ds-dark-theme] .dshr-root,
940
+ [data-theme='dark'] .dshr-root { --dshr-ink-3: var(--dsw-alias-label-tertiary, #a5a49e); }
941
+
942
+ @media (max-width: 980px) {
943
+ .dshr-header { flex-wrap: wrap; row-gap: 4px; }
944
+ .dshr-header-session { display: none; }
945
+ .dshr-dash-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
946
+ .dshr-dash-metrics { gap: 7px; }
947
+ }
948
+ @media (max-width: 760px) {
949
+ .dshr-modal { width: 100vw; height: 100vh; max-height: none; border-radius: 0; }
950
+ .dshr-modal-body { min-height: 0; }
951
+ .dshr-playbar { flex-wrap: wrap; row-gap: 6px; }
952
+ .dshr-scrub { order: 3; flex-basis: 100%; }
953
+ .dshr-clock { order: 4; }
954
+ .dshr-filter-input { width: 100%; order: 5; }
955
+ .dshr-kind-chips { order: 6; }
956
+ .dshr-timeline { flex-direction: column; }
957
+ .dshr-rail { width: 100%; max-width: none; min-width: 0; max-height: 44%; border-right: 0; border-bottom: 1px solid var(--dshr-border); }
958
+ .dshr-detail { flex: 1; }
959
+ .dshr-dash-cards { grid-template-columns: 1fr 1fr; }
960
+ .dshr-dash-row { align-items: flex-start; }
961
+ .dshr-dash-metrics { flex-wrap: wrap; justify-content: flex-end; max-width: 42%; }
962
+ .dshr-diff-grid { grid-template-columns: 1fr; }
963
+ .dshr-header-sub { display: none; }
964
+ }
965
+ @media (prefers-reduced-motion: reduce) {
966
+ .dshr-modal, .dshr-skeleton, .dshr-play-btn[data-playing='true'], .dshr-mascot-state .dshr-mascot { animation: none; }
967
+ .dshr-item, .dshr-bar-in, .dshr-bar-out, .dshr-hbar-fill { transition: none; }
968
+ }
@@ -24,7 +24,10 @@ function ViewerShell(): React.ReactElement {
24
24
  globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false,
25
25
  )
26
26
  return (
27
- <div data-theme={dark ? 'dark' : 'light'} style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
27
+ <div
28
+ data-theme={dark ? 'dark' : 'light'}
29
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
30
+ >
28
31
  <div
29
32
  style={{
30
33
  display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', fontSize: 13,
@@ -33,7 +36,7 @@ function ViewerShell(): React.ReactElement {
33
36
  }}
34
37
  >
35
38
  <strong>dsh-replay</strong>
36
- <span style={{ opacity: 0.6 }}>exported session replay</span>
39
+ <span style={{ opacity: 0.6 }}>exported session review · read-only</span>
37
40
  <span style={{ flex: 1 }} />
38
41
  <a
39
42
  href="https://github.com/MingoZhou/dsh-replay"
@@ -44,7 +47,7 @@ function ViewerShell(): React.ReactElement {
44
47
  GitHub
45
48
  </a>
46
49
  <button
47
- onClick={() => setDark(d => !d)}
50
+ onClick={() => setDark(value => !value)}
48
51
  style={{
49
52
  font: 'inherit', cursor: 'pointer', border: '1px solid ' + (dark ? '#2c2c2a' : '#e1e0d9'),
50
53
  borderRadius: 6, padding: '2px 9px', background: 'transparent', color: 'inherit',
@@ -54,7 +57,25 @@ function ViewerShell(): React.ReactElement {
54
57
  </button>
55
58
  </div>
56
59
  <div style={{ flex: 1, minHeight: 0 }}>
57
- <ReplayApp api={api} sessionId={first} />
60
+ <ReplayApp api={api} sessionId={first} standalone />
61
+ </div>
62
+ <div
63
+ style={{
64
+ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 14px', fontSize: 11,
65
+ borderTop: '1px solid ' + (dark ? '#2c2c2a' : '#e1e0d9'),
66
+ background: dark ? '#0d0d0d' : '#f9f9f7', color: dark ? '#c3c2b7' : '#6b6a66',
67
+ }}
68
+ >
69
+ <span>Exported with dsh-replay. Offline and read-only. If this saved you time, star the repo ★.</span>
70
+ <span style={{ flex: 1 }} />
71
+ <a
72
+ href="https://github.com/MingoZhou/dsh-replay"
73
+ target="_blank"
74
+ rel="noreferrer"
75
+ style={{ color: 'inherit' }}
76
+ >
77
+ Install · Star ★
78
+ </a>
58
79
  </div>
59
80
  </div>
60
81
  )