@prismshadow/mmsp 0.5.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.
Files changed (116) hide show
  1. package/README.md +121 -0
  2. package/dist/ant_messages/client.d.ts +59 -0
  3. package/dist/ant_messages/client.d.ts.map +1 -0
  4. package/dist/ant_messages/client.js +419 -0
  5. package/dist/ant_messages/index.d.ts +2 -0
  6. package/dist/ant_messages/index.d.ts.map +1 -0
  7. package/dist/ant_messages/index.js +18 -0
  8. package/dist/anthropic_official/client.d.ts +63 -0
  9. package/dist/anthropic_official/client.d.ts.map +1 -0
  10. package/dist/anthropic_official/client.js +540 -0
  11. package/dist/anthropic_official/index.d.ts +2 -0
  12. package/dist/anthropic_official/index.d.ts.map +1 -0
  13. package/dist/anthropic_official/index.js +18 -0
  14. package/dist/autoClient.d.ts +96 -0
  15. package/dist/autoClient.d.ts.map +1 -0
  16. package/dist/autoClient.js +258 -0
  17. package/dist/baseClient.d.ts +111 -0
  18. package/dist/baseClient.d.ts.map +1 -0
  19. package/dist/baseClient.js +284 -0
  20. package/dist/deepseek_official/client.d.ts +60 -0
  21. package/dist/deepseek_official/client.d.ts.map +1 -0
  22. package/dist/deepseek_official/client.js +407 -0
  23. package/dist/deepseek_official/index.d.ts +2 -0
  24. package/dist/deepseek_official/index.d.ts.map +1 -0
  25. package/dist/deepseek_official/index.js +18 -0
  26. package/dist/errors.d.ts +84 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +140 -0
  29. package/dist/gemini_generate_content/client.d.ts +86 -0
  30. package/dist/gemini_generate_content/client.d.ts.map +1 -0
  31. package/dist/gemini_generate_content/client.js +809 -0
  32. package/dist/gemini_generate_content/index.d.ts +2 -0
  33. package/dist/gemini_generate_content/index.d.ts.map +1 -0
  34. package/dist/gemini_generate_content/index.js +18 -0
  35. package/dist/gemini_official/client.d.ts +87 -0
  36. package/dist/gemini_official/client.d.ts.map +1 -0
  37. package/dist/gemini_official/client.js +780 -0
  38. package/dist/gemini_official/index.d.ts +2 -0
  39. package/dist/gemini_official/index.d.ts.map +1 -0
  40. package/dist/gemini_official/index.js +18 -0
  41. package/dist/index.d.ts +6 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +44 -0
  44. package/dist/integration/index.d.ts +1 -0
  45. package/dist/integration/index.d.ts.map +1 -0
  46. package/dist/integration/index.js +14 -0
  47. package/dist/integration/playground.d.ts +17 -0
  48. package/dist/integration/playground.d.ts.map +1 -0
  49. package/dist/integration/playground.js +3246 -0
  50. package/dist/integration/tracer.d.ts +188 -0
  51. package/dist/integration/tracer.d.ts.map +1 -0
  52. package/dist/integration/tracer.js +1928 -0
  53. package/dist/legacy.d.ts +13 -0
  54. package/dist/legacy.d.ts.map +1 -0
  55. package/dist/legacy.js +59 -0
  56. package/dist/minimax_official/client.d.ts +43 -0
  57. package/dist/minimax_official/client.d.ts.map +1 -0
  58. package/dist/minimax_official/client.js +346 -0
  59. package/dist/minimax_official/index.d.ts +2 -0
  60. package/dist/minimax_official/index.d.ts.map +1 -0
  61. package/dist/minimax_official/index.js +18 -0
  62. package/dist/moonshot_official/client.d.ts +73 -0
  63. package/dist/moonshot_official/client.d.ts.map +1 -0
  64. package/dist/moonshot_official/client.js +477 -0
  65. package/dist/moonshot_official/index.d.ts +2 -0
  66. package/dist/moonshot_official/index.d.ts.map +1 -0
  67. package/dist/moonshot_official/index.js +18 -0
  68. package/dist/openai_chat/client.d.ts +67 -0
  69. package/dist/openai_chat/client.d.ts.map +1 -0
  70. package/dist/openai_chat/client.js +419 -0
  71. package/dist/openai_chat/index.d.ts +2 -0
  72. package/dist/openai_chat/index.d.ts.map +1 -0
  73. package/dist/openai_chat/index.js +18 -0
  74. package/dist/openai_chat_vllm_adapter/client.d.ts +15 -0
  75. package/dist/openai_chat_vllm_adapter/client.d.ts.map +1 -0
  76. package/dist/openai_chat_vllm_adapter/client.js +118 -0
  77. package/dist/openai_chat_vllm_adapter/index.d.ts +2 -0
  78. package/dist/openai_chat_vllm_adapter/index.d.ts.map +1 -0
  79. package/dist/openai_chat_vllm_adapter/index.js +5 -0
  80. package/dist/openai_embedding/client.d.ts +46 -0
  81. package/dist/openai_embedding/client.d.ts.map +1 -0
  82. package/dist/openai_embedding/client.js +124 -0
  83. package/dist/openai_embedding/index.d.ts +2 -0
  84. package/dist/openai_embedding/index.d.ts.map +1 -0
  85. package/dist/openai_embedding/index.js +18 -0
  86. package/dist/openai_official/client.d.ts +61 -0
  87. package/dist/openai_official/client.d.ts.map +1 -0
  88. package/dist/openai_official/client.js +464 -0
  89. package/dist/openai_official/index.d.ts +2 -0
  90. package/dist/openai_official/index.d.ts.map +1 -0
  91. package/dist/openai_official/index.js +18 -0
  92. package/dist/openai_responses/client.d.ts +61 -0
  93. package/dist/openai_responses/client.d.ts.map +1 -0
  94. package/dist/openai_responses/client.js +449 -0
  95. package/dist/openai_responses/index.d.ts +2 -0
  96. package/dist/openai_responses/index.d.ts.map +1 -0
  97. package/dist/openai_responses/index.js +18 -0
  98. package/dist/registry.d.ts +49 -0
  99. package/dist/registry.d.ts.map +1 -0
  100. package/dist/registry.js +798 -0
  101. package/dist/streamItems.d.ts +36 -0
  102. package/dist/streamItems.d.ts.map +1 -0
  103. package/dist/streamItems.js +183 -0
  104. package/dist/types.d.ts +190 -0
  105. package/dist/types.d.ts.map +1 -0
  106. package/dist/types.js +37 -0
  107. package/dist/utils.d.ts +99 -0
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +312 -0
  110. package/dist/zai_official/client.d.ts +78 -0
  111. package/dist/zai_official/client.d.ts.map +1 -0
  112. package/dist/zai_official/client.js +420 -0
  113. package/dist/zai_official/index.d.ts +2 -0
  114. package/dist/zai_official/index.d.ts.map +1 -0
  115. package/dist/zai_official/index.js +18 -0
  116. package/package.json +68 -0
@@ -0,0 +1,1928 @@
1
+ "use strict";
2
+ // Copyright 2025 Prism Shadow. and/or its affiliates
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ var __importDefault = (this && this.__importDefault) || function (mod) {
49
+ return (mod && mod.__esModule) ? mod : { "default": mod };
50
+ };
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.Tracer = void 0;
53
+ /**
54
+ * Conversation tracer module for saving and viewing conversation history.
55
+ *
56
+ * This module provides functionality to save conversation history to local files
57
+ * and serve them via a web interface for real-time monitoring.
58
+ */
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ const express_1 = __importDefault(require("express"));
62
+ const legacy_1 = require("../legacy");
63
+ // The head every tracer page shares: the playground's tokens, fonts and light and dark themes.
64
+ const TRACER_HEAD = `
65
+ <meta charset="utf-8">
66
+ <meta name="viewport" content="width=device-width, initial-scale=1">
67
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22%3E%3Cstyle%3E.d{fill:%23111116}@media (prefers-color-scheme: dark){.d{fill:%232a2a33}}%3C/style%3E%3Crect width=%2232%22 height=%2232%22 rx=%227%22 class=%22d%22/%3E%3Cpath d=%22M0 16V7a7 7 0 0 1 7-7h9v16Z%22 fill=%22%237c4dff%22/%3E%3Cpath d=%22M16 16h16v9a7 7 0 0 1-7 7h-9Z%22 fill=%22%237c4dff%22/%3E%3Cg fill=%22%23fff%22 font-family=%22ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif%22 font-size=%2210.5%22 font-weight=%22700%22 text-anchor=%22middle%22 dominant-baseline=%22central%22%3E%3Ctext x=%228.5%22 y=%228.5%22%3EM%3C/text%3E%3Ctext x=%2223.5%22 y=%228.5%22%3EM%3C/text%3E%3Ctext x=%228.5%22 y=%2223.5%22%3ES%3C/text%3E%3Ctext x=%2223.5%22 y=%2223.5%22%3EP%3C/text%3E%3C/g%3E%3C/svg%3E">
68
+ <link rel="preconnect" href="https://fonts.googleapis.com">
69
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
70
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap">
71
+ <script>
72
+ // the playground and the tracer share one origin, so they share the theme a reader picked
73
+ try {
74
+ const theme = localStorage.getItem('mmsp.playground.theme');
75
+ if (theme === 'light' || theme === 'dark') {
76
+ document.documentElement.dataset.theme = theme;
77
+ }
78
+ } catch (error) {
79
+ // storage refused: the system theme it is
80
+ }
81
+ </script>
82
+ <style>
83
+ :root {
84
+ --bg: #f5f5f6;
85
+ --surface: #ffffff;
86
+ --raised: #f0f0f2;
87
+ --hover: rgba(20, 22, 28, 0.05);
88
+ --ring: rgba(20, 22, 28, 0.09);
89
+ --ring-strong: rgba(20, 22, 28, 0.17);
90
+ --text: #16181d;
91
+ --muted: #5c616c;
92
+ --subtle: #8a8f99;
93
+ --accent: #2f6fed;
94
+ --accent-soft: rgba(47, 111, 237, 0.14);
95
+ --green: #16945b;
96
+ --green-soft: rgba(22, 148, 91, 0.12);
97
+ --amber: #b16a0a;
98
+ --amber-soft: rgba(177, 106, 10, 0.12);
99
+ --red: #d23b3b;
100
+ --red-soft: rgba(210, 59, 59, 0.1);
101
+ --violet: #7a4fd6;
102
+ --violet-soft: rgba(122, 79, 214, 0.12);
103
+ --indigo: #4a57c9;
104
+ --shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(20, 22, 28, 0.04);
105
+ --shadow-menu: 0 0 0 1px var(--ring), 0 12px 32px -10px rgba(20, 22, 28, 0.22);
106
+ --ease: cubic-bezier(0.23, 1, 0.32, 1);
107
+ --font: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
108
+ --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
109
+ color-scheme: light;
110
+ }
111
+
112
+ @media (prefers-color-scheme: dark) {
113
+ :root:not([data-theme="light"]) {
114
+ --bg: #1b1c1f;
115
+ --surface: #222327;
116
+ --raised: #28292e;
117
+ --hover: rgba(255, 255, 255, 0.05);
118
+ --ring: rgba(255, 255, 255, 0.08);
119
+ --ring-strong: rgba(255, 255, 255, 0.15);
120
+ --text: #eceef1;
121
+ --muted: #a3a8b1;
122
+ --subtle: #6f747e;
123
+ --accent: #4d8ef7;
124
+ --accent-soft: rgba(77, 142, 247, 0.2);
125
+ --green: #43c283;
126
+ --green-soft: rgba(67, 194, 131, 0.14);
127
+ --amber: #e3a646;
128
+ --amber-soft: rgba(227, 166, 70, 0.14);
129
+ --red: #f06a6a;
130
+ --red-soft: rgba(240, 106, 106, 0.14);
131
+ --violet: #a98bf0;
132
+ --violet-soft: rgba(169, 139, 240, 0.16);
133
+ --indigo: #8d97f2;
134
+ --shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.3);
135
+ --shadow-menu: 0 0 0 1px rgba(255, 255, 255, 0.1), 0 16px 36px -10px rgba(0, 0, 0, 0.7);
136
+ color-scheme: dark;
137
+ }
138
+ }
139
+
140
+ :root[data-theme="dark"] {
141
+ --bg: #1b1c1f;
142
+ --surface: #222327;
143
+ --raised: #28292e;
144
+ --hover: rgba(255, 255, 255, 0.05);
145
+ --ring: rgba(255, 255, 255, 0.08);
146
+ --ring-strong: rgba(255, 255, 255, 0.15);
147
+ --text: #eceef1;
148
+ --muted: #a3a8b1;
149
+ --subtle: #6f747e;
150
+ --accent: #4d8ef7;
151
+ --accent-soft: rgba(77, 142, 247, 0.2);
152
+ --green: #43c283;
153
+ --green-soft: rgba(67, 194, 131, 0.14);
154
+ --amber: #e3a646;
155
+ --amber-soft: rgba(227, 166, 70, 0.14);
156
+ --red: #f06a6a;
157
+ --red-soft: rgba(240, 106, 106, 0.14);
158
+ --violet: #a98bf0;
159
+ --violet-soft: rgba(169, 139, 240, 0.16);
160
+ --indigo: #8d97f2;
161
+ --shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.3);
162
+ --shadow-menu: 0 0 0 1px rgba(255, 255, 255, 0.1), 0 16px 36px -10px rgba(0, 0, 0, 0.7);
163
+ color-scheme: dark;
164
+ }
165
+
166
+ *, *::before, *::after {
167
+ box-sizing: border-box;
168
+ }
169
+
170
+ html {
171
+ scroll-padding-top: 72px;
172
+ }
173
+
174
+ body {
175
+ margin: 0;
176
+ min-height: 100vh;
177
+ background: var(--bg);
178
+ color: var(--text);
179
+ font: 14px/1.55 var(--font);
180
+ -webkit-font-smoothing: antialiased;
181
+ text-rendering: optimizeLegibility;
182
+ }
183
+
184
+ a {
185
+ color: inherit;
186
+ text-decoration: none;
187
+ }
188
+
189
+ button {
190
+ font: inherit;
191
+ color: inherit;
192
+ cursor: pointer;
193
+ background: none;
194
+ border: 0;
195
+ padding: 0;
196
+ }
197
+
198
+ svg {
199
+ flex: none;
200
+ }
201
+
202
+ .mono {
203
+ font-family: var(--mono);
204
+ }
205
+
206
+ :focus-visible {
207
+ outline: 2px solid var(--accent);
208
+ outline-offset: 2px;
209
+ }
210
+
211
+ ::selection {
212
+ background: var(--accent-soft);
213
+ }
214
+
215
+ /* top bar */
216
+
217
+ .topbar {
218
+ position: sticky;
219
+ top: 0;
220
+ z-index: 10;
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 16px;
224
+ height: 56px;
225
+ padding: 0 20px;
226
+ background: var(--bg);
227
+ box-shadow: 0 1px 0 var(--ring);
228
+ }
229
+
230
+ .brand {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ flex: none;
235
+ }
236
+
237
+ .brand-mark {
238
+ width: 22px;
239
+ height: 22px;
240
+ flex: none;
241
+ align-self: center;
242
+ }
243
+
244
+ .mark-bg {
245
+ fill: #111116;
246
+ }
247
+
248
+ @media (prefers-color-scheme: dark) {
249
+ :root:not([data-theme="light"]) .mark-bg {
250
+ fill: #2a2a33;
251
+ }
252
+ }
253
+
254
+ :root[data-theme="dark"] .mark-bg {
255
+ fill: #2a2a33;
256
+ }
257
+
258
+ .brand-name {
259
+ font-size: 15px;
260
+ font-weight: 600;
261
+ letter-spacing: -0.01em;
262
+ }
263
+
264
+ .brand-sub {
265
+ color: var(--subtle);
266
+ font-size: 13px;
267
+ }
268
+
269
+ .page-nav {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ margin-bottom: 20px;
274
+ }
275
+
276
+ .nav-buttons {
277
+ display: flex;
278
+ flex: none;
279
+ gap: 2px;
280
+ }
281
+
282
+ .nav-btn {
283
+ display: grid;
284
+ place-items: center;
285
+ width: 32px;
286
+ height: 32px;
287
+ border-radius: 8px;
288
+ color: var(--muted);
289
+ transition: color 0.15s, background-color 0.15s;
290
+ }
291
+
292
+ .nav-btn:hover {
293
+ color: var(--text);
294
+ background: var(--hover);
295
+ }
296
+
297
+ .nav-btn:disabled, .nav-btn[aria-disabled="true"] {
298
+ color: var(--subtle);
299
+ opacity: 0.45;
300
+ cursor: default;
301
+ pointer-events: none;
302
+ }
303
+
304
+ .address {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 2px;
308
+ flex: 1;
309
+ min-width: 0;
310
+ height: 34px;
311
+ padding: 0 6px;
312
+ overflow: hidden;
313
+ border-radius: 8px;
314
+ background: var(--surface);
315
+ box-shadow: var(--shadow-card);
316
+ font-size: 13px;
317
+ white-space: nowrap;
318
+ }
319
+
320
+ .address-icon {
321
+ display: grid;
322
+ place-items: center;
323
+ flex: none;
324
+ width: 24px;
325
+ color: var(--subtle);
326
+ }
327
+
328
+ .crumb {
329
+ flex: none;
330
+ padding: 3px 6px;
331
+ border-radius: 5px;
332
+ color: var(--muted);
333
+ transition: color 0.15s, background-color 0.15s;
334
+ }
335
+
336
+ a.crumb:hover {
337
+ color: var(--text);
338
+ background: var(--hover);
339
+ }
340
+
341
+ .crumb-current {
342
+ flex: 0 1 auto;
343
+ min-width: 0;
344
+ overflow: hidden;
345
+ text-overflow: ellipsis;
346
+ color: var(--text);
347
+ font-weight: 500;
348
+ }
349
+
350
+ .crumb-sep {
351
+ display: grid;
352
+ place-items: center;
353
+ flex: none;
354
+ color: var(--subtle);
355
+ }
356
+
357
+ .topbar-actions {
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 8px;
361
+ margin-left: auto;
362
+ }
363
+
364
+ .ghost-btn {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 6px;
368
+ height: 32px;
369
+ padding: 0 10px;
370
+ border-radius: 8px;
371
+ color: var(--muted);
372
+ font-size: 13px;
373
+ font-weight: 500;
374
+ white-space: nowrap;
375
+ transition: color 0.15s, background-color 0.15s;
376
+ }
377
+
378
+ .ghost-btn:hover {
379
+ color: var(--text);
380
+ background: var(--hover);
381
+ }
382
+
383
+ .segmented {
384
+ position: relative;
385
+ display: flex;
386
+ padding: 3px;
387
+ border-radius: 9px;
388
+ background: var(--raised);
389
+ box-shadow: inset 0 0 0 1px var(--ring);
390
+ }
391
+
392
+ .segmented button, .segmented a {
393
+ position: relative;
394
+ z-index: 1;
395
+ display: grid;
396
+ place-items: center;
397
+ min-width: 0;
398
+ height: 26px;
399
+ padding: 0 10px;
400
+ border-radius: 6px;
401
+ color: var(--muted);
402
+ font-size: 12.5px;
403
+ font-weight: 500;
404
+ white-space: nowrap;
405
+ transition: color 0.15s, background-color 0.15s;
406
+ }
407
+
408
+ .segmented button:hover, .segmented a:hover {
409
+ color: var(--text);
410
+ }
411
+
412
+ .segmented [aria-checked="true"], .segmented [aria-current="true"] {
413
+ color: var(--text);
414
+ }
415
+
416
+ .segmented a[aria-current="true"] {
417
+ background: var(--surface);
418
+ box-shadow: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.12);
419
+ }
420
+
421
+ .seg-thumb {
422
+ position: absolute;
423
+ top: 3px;
424
+ bottom: 3px;
425
+ left: 0;
426
+ width: 0;
427
+ border-radius: 6px;
428
+ background: var(--surface);
429
+ box-shadow: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.12);
430
+ transition: transform 0.25s var(--ease), width 0.25s var(--ease);
431
+ }
432
+
433
+ .theme-toggle button {
434
+ width: 29px;
435
+ padding: 0;
436
+ }
437
+
438
+ /* pages */
439
+
440
+ .page {
441
+ max-width: 920px;
442
+ margin: 0 auto;
443
+ padding: 32px 20px 64px;
444
+ }
445
+
446
+ .page-head {
447
+ display: flex;
448
+ align-items: flex-end;
449
+ justify-content: space-between;
450
+ gap: 16px;
451
+ margin-bottom: 20px;
452
+ }
453
+
454
+ .page-head h1 {
455
+ margin: 0;
456
+ font-size: 22px;
457
+ font-weight: 600;
458
+ letter-spacing: -0.015em;
459
+ overflow-wrap: anywhere;
460
+ }
461
+
462
+ .page-meta {
463
+ display: flex;
464
+ flex-wrap: wrap;
465
+ gap: 4px 14px;
466
+ margin: 6px 0 0;
467
+ color: var(--subtle);
468
+ font-size: 13px;
469
+ }
470
+
471
+ .page-meta b {
472
+ color: var(--muted);
473
+ font-weight: 500;
474
+ }
475
+
476
+ .card {
477
+ background: var(--surface);
478
+ border-radius: 12px;
479
+ box-shadow: var(--shadow-card);
480
+ }
481
+
482
+ /* directory */
483
+
484
+ .list {
485
+ overflow: hidden;
486
+ }
487
+
488
+ .row {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 12px;
492
+ padding: 10px 16px;
493
+ transition: background-color 0.12s;
494
+ }
495
+
496
+ .row + .row {
497
+ box-shadow: inset 0 1px 0 var(--ring);
498
+ }
499
+
500
+ .row:hover {
501
+ background: var(--hover);
502
+ }
503
+
504
+ .row-icon {
505
+ display: grid;
506
+ place-items: center;
507
+ width: 28px;
508
+ height: 28px;
509
+ border-radius: 7px;
510
+ background: var(--raised);
511
+ color: var(--muted);
512
+ }
513
+
514
+ .row-icon.dir {
515
+ background: var(--accent-soft);
516
+ color: var(--accent);
517
+ }
518
+
519
+ .row-name {
520
+ flex: 1;
521
+ min-width: 0;
522
+ overflow: hidden;
523
+ text-overflow: ellipsis;
524
+ white-space: nowrap;
525
+ font-weight: 500;
526
+ }
527
+
528
+ .row-size, .row-time {
529
+ flex: none;
530
+ color: var(--subtle);
531
+ font-family: var(--mono);
532
+ font-size: 12px;
533
+ font-variant-numeric: tabular-nums;
534
+ }
535
+
536
+ .row-size {
537
+ width: 72px;
538
+ text-align: right;
539
+ }
540
+
541
+ .row-time {
542
+ width: 150px;
543
+ text-align: right;
544
+ }
545
+
546
+ .empty {
547
+ padding: 48px 20px;
548
+ color: var(--muted);
549
+ text-align: center;
550
+ }
551
+
552
+ .empty code {
553
+ padding: 1px 5px;
554
+ border-radius: 5px;
555
+ background: var(--raised);
556
+ font-family: var(--mono);
557
+ font-size: 12.5px;
558
+ }
559
+
560
+ /* trace */
561
+
562
+ .rail {
563
+ position: fixed;
564
+ top: 76px;
565
+ right: 20px;
566
+ z-index: 20;
567
+ display: flex;
568
+ flex-direction: column;
569
+ align-items: flex-end;
570
+ gap: 8px;
571
+ }
572
+
573
+ .rail-toggle {
574
+ display: none;
575
+ align-items: center;
576
+ gap: 8px;
577
+ height: 32px;
578
+ padding: 0 10px 0 12px;
579
+ border-radius: 999px;
580
+ background: var(--surface);
581
+ box-shadow: var(--shadow-menu);
582
+ color: var(--muted);
583
+ font-size: 12.5px;
584
+ font-weight: 500;
585
+ }
586
+
587
+ .rail-toggle .mono {
588
+ color: var(--text);
589
+ font-size: 12px;
590
+ }
591
+
592
+ .rail-panel {
593
+ width: 200px;
594
+ max-height: calc(100vh - 120px);
595
+ overflow-y: auto;
596
+ padding: 12px 12px 10px;
597
+ border-radius: 12px;
598
+ background: var(--surface);
599
+ box-shadow: var(--shadow-menu);
600
+ }
601
+
602
+ @keyframes panel-in {
603
+ from {
604
+ opacity: 0;
605
+ transform: translateY(-4px) scale(0.98);
606
+ }
607
+ }
608
+
609
+ .rail-title {
610
+ margin-bottom: 8px;
611
+ color: var(--subtle);
612
+ font-size: 12px;
613
+ font-weight: 500;
614
+ }
615
+
616
+ .rail-list {
617
+ margin: 0;
618
+ padding: 0;
619
+ list-style: none;
620
+ box-shadow: inset 1px 0 0 var(--ring);
621
+ }
622
+
623
+ .rail-list a {
624
+ display: flex;
625
+ align-items: baseline;
626
+ justify-content: space-between;
627
+ gap: 8px;
628
+ padding: 4px 0 4px 12px;
629
+ border-left: 1.5px solid transparent;
630
+ color: var(--muted);
631
+ font-size: 13px;
632
+ transition: color 0.15s, border-color 0.15s;
633
+ }
634
+
635
+ .rail-list a:hover {
636
+ color: var(--text);
637
+ }
638
+
639
+ .rail-list a.active {
640
+ border-left-color: var(--accent);
641
+ color: var(--text);
642
+ font-weight: 500;
643
+ }
644
+
645
+ .rail-list a span {
646
+ color: var(--subtle);
647
+ font-family: var(--mono);
648
+ font-size: 11px;
649
+ font-weight: 400;
650
+ }
651
+
652
+ @media (max-width: 1400px) {
653
+ .rail-toggle {
654
+ display: inline-flex;
655
+ }
656
+
657
+ .rail-panel {
658
+ display: none;
659
+ }
660
+
661
+ .rail.open .rail-panel {
662
+ display: block;
663
+ transform-origin: top right;
664
+ animation: panel-in 0.18s var(--ease);
665
+ }
666
+
667
+ .rail.open .rail-toggle .chevron {
668
+ transform: rotate(180deg);
669
+ }
670
+ }
671
+
672
+ .fold summary, .msg-card summary {
673
+ list-style: none;
674
+ cursor: pointer;
675
+ }
676
+
677
+ .fold summary::-webkit-details-marker, .msg-card summary::-webkit-details-marker {
678
+ display: none;
679
+ }
680
+
681
+ .chevron {
682
+ color: var(--subtle);
683
+ transition: transform 0.25s var(--ease);
684
+ }
685
+
686
+ details[open] > summary .chevron {
687
+ transform: rotate(180deg);
688
+ }
689
+
690
+ .config {
691
+ margin-bottom: 20px;
692
+ }
693
+
694
+ .card-head {
695
+ padding: 12px 16px;
696
+ box-shadow: inset 0 -1px 0 var(--ring);
697
+ color: var(--muted);
698
+ font-size: 12.5px;
699
+ font-weight: 500;
700
+ }
701
+
702
+ .kv {
703
+ margin: 0;
704
+ }
705
+
706
+ .kv-row {
707
+ display: grid;
708
+ grid-template-columns: 160px 1fr;
709
+ gap: 16px;
710
+ padding: 9px 16px;
711
+ }
712
+
713
+ .kv-row + .kv-row {
714
+ box-shadow: inset 0 1px 0 var(--ring);
715
+ }
716
+
717
+ .kv-row dt {
718
+ color: var(--muted);
719
+ font-family: var(--mono);
720
+ font-size: 12.5px;
721
+ }
722
+
723
+ .kv-row dd {
724
+ margin: 0;
725
+ min-width: 0;
726
+ font-family: var(--mono);
727
+ font-size: 12.5px;
728
+ overflow-wrap: anywhere;
729
+ white-space: pre-wrap;
730
+ }
731
+
732
+ .fold summary {
733
+ display: inline-flex;
734
+ align-items: center;
735
+ gap: 4px;
736
+ color: var(--accent);
737
+ font-family: var(--font);
738
+ font-size: 12.5px;
739
+ font-weight: 500;
740
+ }
741
+
742
+ .fold pre {
743
+ margin: 8px 0 0;
744
+ padding: 10px 12px;
745
+ max-height: 360px;
746
+ overflow: auto;
747
+ border-radius: 8px;
748
+ background: var(--raised);
749
+ font-family: var(--mono);
750
+ font-size: 12px;
751
+ line-height: 1.6;
752
+ white-space: pre-wrap;
753
+ }
754
+
755
+ .msg-card {
756
+ margin-bottom: 14px;
757
+ overflow: hidden;
758
+ }
759
+
760
+ .msg-summary {
761
+ display: flex;
762
+ flex-wrap: wrap;
763
+ align-items: center;
764
+ gap: 6px 14px;
765
+ padding: 11px 16px;
766
+ color: var(--subtle);
767
+ font-size: 12.5px;
768
+ transition: background-color 0.12s;
769
+ }
770
+
771
+ .msg-summary:hover {
772
+ background: var(--hover);
773
+ }
774
+
775
+ details[open] > .msg-summary {
776
+ box-shadow: inset 0 -1px 0 var(--ring);
777
+ }
778
+
779
+ .role {
780
+ font-size: 12px;
781
+ font-weight: 600;
782
+ letter-spacing: 0.06em;
783
+ text-transform: uppercase;
784
+ }
785
+
786
+ .role-user { color: var(--accent); }
787
+ .role-assistant { color: var(--green); }
788
+
789
+ .msg-summary .spacer {
790
+ flex: 1;
791
+ }
792
+
793
+ .msg-summary .num {
794
+ font-family: var(--mono);
795
+ font-size: 11.5px;
796
+ font-variant-numeric: tabular-nums;
797
+ }
798
+
799
+ .msg-body {
800
+ padding: 4px 16px 14px;
801
+ }
802
+
803
+ /* a list, carried by alignment: the type in its kind's color on the left, the content beside it */
804
+ .item {
805
+ --k: var(--muted);
806
+ display: grid;
807
+ grid-template-columns: 136px minmax(0, 1fr);
808
+ column-gap: 16px;
809
+ padding: 12px 0;
810
+ }
811
+
812
+ .item + .item {
813
+ box-shadow: inset 0 1px 0 var(--ring);
814
+ }
815
+
816
+ .item > :not(.item-head) {
817
+ grid-column: 2;
818
+ }
819
+
820
+ .kind-text { --k: var(--muted); }
821
+ .kind-thinking, .kind-inline_thinking { --k: var(--accent); }
822
+ .kind-tool_call { --k: var(--amber); }
823
+ .kind-tool_result { --k: var(--green); }
824
+ .kind-image_url, .kind-inline_data { --k: var(--violet); }
825
+ .kind-embedding { --k: var(--indigo); }
826
+
827
+ .item-head {
828
+ grid-column: 1;
829
+ grid-row: 1 / span 4;
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 2px;
833
+ padding-top: 2px;
834
+ }
835
+
836
+ .item-type {
837
+ color: var(--k);
838
+ font-family: var(--mono);
839
+ font-size: 11.5px;
840
+ font-weight: 500;
841
+ }
842
+
843
+ .item-id {
844
+ color: var(--subtle);
845
+ font-family: var(--mono);
846
+ font-size: 11px;
847
+ overflow-wrap: anywhere;
848
+ }
849
+
850
+ .item-text {
851
+ white-space: pre-wrap;
852
+ overflow-wrap: anywhere;
853
+ line-height: 1.65;
854
+ }
855
+
856
+ .item-thinking {
857
+ color: var(--muted);
858
+ font-size: 13px;
859
+ line-height: 1.6;
860
+ white-space: pre-wrap;
861
+ overflow-wrap: anywhere;
862
+ }
863
+
864
+ .item-note {
865
+ color: var(--subtle);
866
+ font-family: var(--mono);
867
+ font-size: 12px;
868
+ }
869
+
870
+ .item-note + * {
871
+ margin-top: 8px;
872
+ }
873
+
874
+ .item-img {
875
+ display: block;
876
+ max-width: min(100%, 360px);
877
+ max-height: 280px;
878
+ border-radius: 8px;
879
+ box-shadow: 0 0 0 1px var(--ring);
880
+ }
881
+
882
+ .item-images {
883
+ display: flex;
884
+ flex-wrap: wrap;
885
+ gap: 8px;
886
+ margin-top: 10px;
887
+ }
888
+
889
+ .item-audio {
890
+ display: block;
891
+ width: min(100%, 360px);
892
+ height: 36px;
893
+ }
894
+
895
+ .tool-sig, .result-text, .embedding {
896
+ font-family: var(--mono);
897
+ font-size: 12.5px;
898
+ line-height: 1.6;
899
+ white-space: pre-wrap;
900
+ overflow-wrap: anywhere;
901
+ }
902
+
903
+ .msg-foot {
904
+ display: flex;
905
+ flex-wrap: wrap;
906
+ align-items: center;
907
+ gap: 6px 14px;
908
+ margin-top: 16px;
909
+ padding-top: 12px;
910
+ box-shadow: inset 0 1px 0 var(--ring);
911
+ color: var(--subtle);
912
+ font-size: 12px;
913
+ }
914
+
915
+ .reason {
916
+ display: inline-flex;
917
+ align-items: center;
918
+ gap: 6px;
919
+ height: 22px;
920
+ padding: 0 8px;
921
+ border-radius: 999px;
922
+ background: var(--raised);
923
+ color: var(--muted);
924
+ font-family: var(--mono);
925
+ font-size: 11.5px;
926
+ }
927
+
928
+ .reason::before {
929
+ content: "";
930
+ width: 6px;
931
+ height: 6px;
932
+ border-radius: 50%;
933
+ background: currentColor;
934
+ }
935
+
936
+ .reason-stop { background: var(--green-soft); color: var(--green); }
937
+ .reason-tool_call { background: var(--amber-soft); color: var(--amber); }
938
+ .reason-length, .reason-unknown { background: var(--red-soft); color: var(--red); }
939
+
940
+ .usage {
941
+ display: inline-flex;
942
+ flex-wrap: wrap;
943
+ gap: 4px 12px;
944
+ font-family: var(--mono);
945
+ font-size: 11.5px;
946
+ font-variant-numeric: tabular-nums;
947
+ }
948
+
949
+ .usage b {
950
+ color: var(--muted);
951
+ font-weight: 500;
952
+ }
953
+
954
+ .text-file {
955
+ margin: 0;
956
+ padding: 16px 18px;
957
+ overflow-x: auto;
958
+ font-family: var(--mono);
959
+ font-size: 12.5px;
960
+ line-height: 1.65;
961
+ white-space: pre-wrap;
962
+ overflow-wrap: anywhere;
963
+ }
964
+
965
+ @media (max-width: 640px) {
966
+ .topbar {
967
+ gap: 10px;
968
+ padding: 0 12px;
969
+ }
970
+
971
+ .label-wide {
972
+ display: none;
973
+ }
974
+
975
+ .page {
976
+ padding: 24px 16px 48px;
977
+ }
978
+
979
+ .row-time {
980
+ display: none;
981
+ }
982
+
983
+ .kv-row {
984
+ grid-template-columns: 1fr;
985
+ gap: 2px;
986
+ }
987
+
988
+ .item {
989
+ grid-template-columns: minmax(0, 1fr);
990
+ row-gap: 4px;
991
+ }
992
+
993
+ .item > :not(.item-head), .item-head {
994
+ grid-column: 1;
995
+ grid-row: auto;
996
+ }
997
+
998
+ .item-head {
999
+ flex-direction: row;
1000
+ gap: 8px;
1001
+ }
1002
+
1003
+ .page-head {
1004
+ flex-direction: column;
1005
+ align-items: flex-start;
1006
+ }
1007
+
1008
+ .nav-btn.forward {
1009
+ display: none;
1010
+ }
1011
+
1012
+ .rail {
1013
+ top: auto;
1014
+ bottom: 16px;
1015
+ right: 16px;
1016
+ flex-direction: column-reverse;
1017
+ }
1018
+ }
1019
+
1020
+ @media (prefers-reduced-motion: reduce) {
1021
+ *, *::before, *::after {
1022
+ animation-duration: 0.01ms !important;
1023
+ transition-duration: 0.01ms !important;
1024
+ }
1025
+ }
1026
+ </style>
1027
+ `;
1028
+ const TRACER_SCRIPT = `
1029
+ <script>
1030
+ function updateSegmentThumb(root) {
1031
+ const thumb = root && root.querySelector('.seg-thumb');
1032
+ const checked = root && root.querySelector('[aria-checked="true"]');
1033
+ if (!thumb || !checked || !checked.offsetWidth) {
1034
+ return;
1035
+ }
1036
+ thumb.style.width = checked.offsetWidth + 'px';
1037
+ thumb.style.transform = 'translateX(' + checked.offsetLeft + 'px)';
1038
+ }
1039
+
1040
+ function updateThemeToggle() {
1041
+ const stored = document.documentElement.dataset.theme;
1042
+ const theme = stored || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
1043
+ document.querySelectorAll('#themeToggle [data-theme-choice]').forEach(function (button) {
1044
+ button.setAttribute('aria-checked', button.dataset.themeChoice === theme ? 'true' : 'false');
1045
+ });
1046
+ updateSegmentThumb(document.getElementById('themeToggle'));
1047
+ }
1048
+
1049
+ function setTheme(theme) {
1050
+ document.documentElement.dataset.theme = theme;
1051
+ try {
1052
+ localStorage.setItem('mmsp.playground.theme', theme);
1053
+ } catch (error) {
1054
+ // a browser that refuses storage keeps the choice for this page only
1055
+ }
1056
+ updateThemeToggle();
1057
+ }
1058
+
1059
+ // back and forward grey out, as in a file explorer, when the history has nowhere to go
1060
+ (function () {
1061
+ const back = document.getElementById('navBack');
1062
+ const forward = document.getElementById('navForward');
1063
+ const navigation = window.navigation;
1064
+ if (back) {
1065
+ back.disabled = navigation ? !navigation.canGoBack : history.length <= 1;
1066
+ }
1067
+ if (forward && navigation) {
1068
+ forward.disabled = !navigation.canGoForward;
1069
+ }
1070
+ })();
1071
+
1072
+ function toggleRail(open) {
1073
+ const rail = document.getElementById('rail');
1074
+ const isOpen = typeof open === 'boolean' ? open : !rail.classList.contains('open');
1075
+ rail.classList.toggle('open', isOpen);
1076
+ rail.querySelector('.rail-toggle').setAttribute('aria-expanded', isOpen ? 'true' : 'false');
1077
+ }
1078
+
1079
+ document.addEventListener('click', function (event) {
1080
+ const rail = document.getElementById('rail');
1081
+ if (rail && rail.classList.contains('open') && !rail.contains(event.target)) {
1082
+ toggleRail(false);
1083
+ }
1084
+ });
1085
+
1086
+ updateThemeToggle();
1087
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeToggle);
1088
+ document.fonts.ready.then(updateThemeToggle);
1089
+
1090
+ // the rail marks the round whose first message is nearest the top of the view
1091
+ const railLinks = Array.from(document.querySelectorAll('.rail-list a'));
1092
+ if (railLinks.length) {
1093
+ const targets = railLinks.map(function (link) {
1094
+ return document.querySelector(link.getAttribute('href'));
1095
+ });
1096
+ const markActive = function () {
1097
+ let active = 0;
1098
+ targets.forEach(function (target, index) {
1099
+ if (target && target.getBoundingClientRect().top < 140) {
1100
+ active = index;
1101
+ }
1102
+ });
1103
+ // at the bottom of the page the last round is the one being read, however high it sits
1104
+ if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4) {
1105
+ active = targets.length - 1;
1106
+ }
1107
+ railLinks.forEach(function (link, index) {
1108
+ link.classList.toggle('active', index === active);
1109
+ });
1110
+ const current = document.getElementById('railCurrent');
1111
+ if (current) {
1112
+ current.textContent = (active + 1) + ' / ' + railLinks.length;
1113
+ }
1114
+ };
1115
+ railLinks.forEach(function (link) {
1116
+ link.addEventListener('click', function () {
1117
+ toggleRail(false);
1118
+ });
1119
+ });
1120
+ window.addEventListener('scroll', markActive, { passive: true });
1121
+ markActive();
1122
+ }
1123
+ </script>
1124
+ `;
1125
+ const ICONS = {
1126
+ mark: '<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" class="mark-bg"></rect><path d="M0 16V7a7 7 0 0 1 7-7h9v16Z" fill="#7c4dff"></path><path d="M16 16h16v9a7 7 0 0 1-7 7h-9Z" fill="#7c4dff"></path><g fill="#fff" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="10.5" font-weight="700" text-anchor="middle" dominant-baseline="central"><text x="8.5" y="8.5">M</text><text x="23.5" y="8.5">M</text><text x="8.5" y="23.5">S</text><text x="23.5" y="23.5">P</text></g></svg>',
1127
+ github: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.02a9.6 9.6 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"></path></svg>',
1128
+ sun: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path></svg>',
1129
+ moon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"></path></svg>',
1130
+ folder: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg>',
1131
+ file: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"></path><path d="M14 3v5h5M9 13h6M9 17h4"></path></svg>',
1132
+ chevron: '<svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>',
1133
+ arrow_left: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"></path></svg>',
1134
+ arrow_right: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>',
1135
+ arrow_up: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>',
1136
+ crumb: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg>',
1137
+ };
1138
+ /**
1139
+ * Tracer for saving conversation history to local files.
1140
+ *
1141
+ * This class handles saving conversation history to files in a cache directory.
1142
+ */
1143
+ class Tracer {
1144
+ /**
1145
+ * Initialize the tracer.
1146
+ *
1147
+ * @param cacheDir - Directory to store conversation history files
1148
+ */
1149
+ constructor(cacheDir) {
1150
+ this.cacheDir = path.resolve(cacheDir || process.env.MMSP_CACHE_DIR || "cache");
1151
+ this._ensureDirectoryExists(this.cacheDir);
1152
+ }
1153
+ /**
1154
+ * Ensure directory exists, create if it doesn't.
1155
+ *
1156
+ * @param dirPath - Directory path to create
1157
+ */
1158
+ _ensureDirectoryExists(dirPath) {
1159
+ if (!fs.existsSync(dirPath)) {
1160
+ fs.mkdirSync(dirPath, { recursive: true });
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Recursively serialize objects for JSON, converting Buffer to base64.
1165
+ *
1166
+ * @param obj - Object to serialize
1167
+ * @returns JSON-serializable object
1168
+ */
1169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1170
+ _serializeForJson(obj) {
1171
+ if (Buffer.isBuffer(obj)) {
1172
+ return obj.toString("base64");
1173
+ }
1174
+ else if (obj && typeof obj === "object") {
1175
+ if (Array.isArray(obj)) {
1176
+ return obj.map((item) => this._serializeForJson(item));
1177
+ }
1178
+ else {
1179
+ const result = {};
1180
+ for (const [key, value] of Object.entries(obj)) {
1181
+ result[key] = this._serializeForJson(value);
1182
+ }
1183
+ return result;
1184
+ }
1185
+ }
1186
+ return obj;
1187
+ }
1188
+ /**
1189
+ * Return whether browsers can usually play this MIME type directly.
1190
+ *
1191
+ * @param mimeType - MIME type to inspect
1192
+ * @returns Whether the browser can typically play the audio type
1193
+ */
1194
+ _isBrowserPlayableAudioMimeType(mimeType) {
1195
+ return new Set([
1196
+ "audio/wav",
1197
+ "audio/x-wav",
1198
+ "audio/mpeg",
1199
+ "audio/mp3",
1200
+ "audio/ogg",
1201
+ "audio/webm",
1202
+ "audio/flac",
1203
+ "audio/aac",
1204
+ "audio/mp4",
1205
+ ]).has((mimeType || "").toLowerCase());
1206
+ }
1207
+ /**
1208
+ * Decode an inline_data payload to raw bytes.
1209
+ *
1210
+ * @param item - inline_data content item
1211
+ * @returns Raw payload bytes
1212
+ */
1213
+ _decodeInlineData(item) {
1214
+ if (typeof item.data === "string") {
1215
+ return Buffer.from(item.data, "base64");
1216
+ }
1217
+ return item.data || Buffer.alloc(0);
1218
+ }
1219
+ /**
1220
+ * Wrap raw PCM bytes in a WAV header using Gemini TTS defaults.
1221
+ *
1222
+ * @param item - inline_data content item
1223
+ * @returns WAV bytes
1224
+ */
1225
+ _buildWaveBytesFromPcm(item) {
1226
+ const pcmBytes = this._decodeInlineData(item);
1227
+ const channels = 1;
1228
+ const sampleRate = 24000;
1229
+ const bitsPerSample = 16;
1230
+ const byteRate = (sampleRate * channels * bitsPerSample) / 8;
1231
+ const blockAlign = (channels * bitsPerSample) / 8;
1232
+ const header = Buffer.alloc(44);
1233
+ header.write("RIFF", 0, "ascii");
1234
+ header.writeUInt32LE(36 + pcmBytes.length, 4);
1235
+ header.write("WAVE", 8, "ascii");
1236
+ header.write("fmt ", 12, "ascii");
1237
+ header.writeUInt32LE(16, 16);
1238
+ header.writeUInt16LE(1, 20);
1239
+ header.writeUInt16LE(channels, 22);
1240
+ header.writeUInt32LE(sampleRate, 24);
1241
+ header.writeUInt32LE(byteRate, 28);
1242
+ header.writeUInt16LE(blockAlign, 32);
1243
+ header.writeUInt16LE(bitsPerSample, 34);
1244
+ header.write("data", 36, "ascii");
1245
+ header.writeUInt32LE(pcmBytes.length, 40);
1246
+ return Buffer.concat([header, pcmBytes]);
1247
+ }
1248
+ /**
1249
+ * Format inline_data metadata without emitting raw payloads.
1250
+ *
1251
+ * @param item - inline_data content item
1252
+ * @returns Human-readable summary of the inline payload
1253
+ */
1254
+ _formatInlineDataSummary(item, isThinking) {
1255
+ const mimeType = item.mime_type || "application/octet-stream";
1256
+ const data = item.data;
1257
+ let byteCount = 0;
1258
+ if (typeof data === "string") {
1259
+ byteCount = Buffer.from(data, "base64").length;
1260
+ }
1261
+ else if (Buffer.isBuffer(data)) {
1262
+ byteCount = data.length;
1263
+ }
1264
+ const kbCount = byteCount / 1024;
1265
+ const mbCount = byteCount / (1024 * 1024);
1266
+ let label = isThinking ? "Thinking " : "";
1267
+ if (mimeType.startsWith("image/")) {
1268
+ label += "Inline Image";
1269
+ }
1270
+ else if (mimeType.startsWith("audio/")) {
1271
+ label += "Inline Audio";
1272
+ }
1273
+ else {
1274
+ label += "Inline Data";
1275
+ }
1276
+ if (kbCount < 1000) {
1277
+ return `${label}: ${mimeType} (${kbCount.toFixed(2)} KB)`;
1278
+ }
1279
+ return `${label}: ${mimeType} (${mbCount.toFixed(2)} MB)`;
1280
+ }
1281
+ _formatEmbeddingPreview(item) {
1282
+ const preview = (item.embedding || [])
1283
+ .slice(0, 5)
1284
+ .map((value) => String(value))
1285
+ .join(", ");
1286
+ return `Embedding: [${preview}]`;
1287
+ }
1288
+ /**
1289
+ * Build a data URL for inline_data content.
1290
+ *
1291
+ * @param item - inline_data content item
1292
+ * @returns Data URL string
1293
+ */
1294
+ _inlineDataUrl(item) {
1295
+ const mimeType = item.mime_type || "application/octet-stream";
1296
+ let rawBytes = this._decodeInlineData(item);
1297
+ let normalizedMimeType = mimeType;
1298
+ if (mimeType.startsWith("audio/")) {
1299
+ if (!this._isBrowserPlayableAudioMimeType(mimeType)) {
1300
+ normalizedMimeType = "audio/wav";
1301
+ rawBytes = this._buildWaveBytesFromPcm(item);
1302
+ }
1303
+ return `data:${normalizedMimeType};base64,${rawBytes.toString("base64")}`;
1304
+ }
1305
+ return `data:${normalizedMimeType};base64,${rawBytes.toString("base64")}`;
1306
+ }
1307
+ /**
1308
+ * Return whether inline_data should be rendered as audio.
1309
+ *
1310
+ * @param item - inline_data content item
1311
+ * @returns Whether the payload is audio
1312
+ */
1313
+ _inlineDataIsAudio(item) {
1314
+ return (item.mime_type || "").toLowerCase().startsWith("audio/");
1315
+ }
1316
+ /**
1317
+ * Return the browser-facing audio MIME type after any WAV fallback.
1318
+ *
1319
+ * @param item - inline_data content item
1320
+ * @returns Playable audio MIME type
1321
+ */
1322
+ _inlineDataAudioType(item) {
1323
+ const mimeType = item.mime_type || "application/octet-stream";
1324
+ if (this._isBrowserPlayableAudioMimeType(mimeType)) {
1325
+ return mimeType;
1326
+ }
1327
+ return "audio/wav";
1328
+ }
1329
+ /**
1330
+ * Save conversation history to files.
1331
+ *
1332
+ * @param history - List of UniMessage objects representing the conversation
1333
+ * @param fileId - File identifier without extension (e.g., "agent1/00001")
1334
+ * @param config - The UniConfig used for this conversation
1335
+ */
1336
+ saveHistory(model, history, fileId, config) {
1337
+ const filePathBase = path.join(this.cacheDir, fileId);
1338
+ const dirPath = path.dirname(filePathBase);
1339
+ this._ensureDirectoryExists(dirPath);
1340
+ const configWithModel = {
1341
+ ...config,
1342
+ model,
1343
+ };
1344
+ const jsonPath = filePathBase + ".json";
1345
+ const jsonData = {
1346
+ history: this._serializeForJson(history),
1347
+ config: this._serializeForJson(configWithModel),
1348
+ timestamp: new Date().toISOString(),
1349
+ };
1350
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), "utf-8");
1351
+ const txtPath = filePathBase + ".txt";
1352
+ const formattedContent = this._formatHistory(history, configWithModel);
1353
+ fs.writeFileSync(txtPath, formattedContent, "utf-8");
1354
+ }
1355
+ /**
1356
+ * Format conversation history in a readable text format.
1357
+ *
1358
+ * @param history - List of UniMessage objects
1359
+ * @param config - The UniConfig used for this conversation
1360
+ * @returns Formatted string representation of the conversation
1361
+ */
1362
+ _formatHistory(history, config) {
1363
+ const lines = [];
1364
+ lines.push("=".repeat(80));
1365
+ lines.push(`Conversation History - ${new Date().toLocaleString()}`);
1366
+ lines.push("=".repeat(80));
1367
+ lines.push("");
1368
+ lines.push("Configuration:");
1369
+ for (const [key, value] of Object.entries(config)) {
1370
+ if (key !== "trace_id") {
1371
+ if (key === "tools" && Array.isArray(value)) {
1372
+ lines.push(` ${key}:`);
1373
+ lines.push(` ${JSON.stringify(value, null, 2)}`);
1374
+ }
1375
+ else {
1376
+ lines.push(` ${key}: ${value}`);
1377
+ }
1378
+ }
1379
+ }
1380
+ lines.push("");
1381
+ for (let i = 0; i < history.length; i++) {
1382
+ const message = history[i];
1383
+ const role = message.role.toUpperCase();
1384
+ lines.push(`[${i + 1}] ${role}:`);
1385
+ lines.push("-".repeat(80));
1386
+ for (const item of message.content_items) {
1387
+ if (item.type === "text.done") {
1388
+ lines.push(`Text: ${item.text}`);
1389
+ }
1390
+ else if (item.type === "thinking.done") {
1391
+ lines.push(`Thinking: ${item.thinking}`);
1392
+ }
1393
+ else if (item.type === "inline_thinking.done") {
1394
+ lines.push(this._formatInlineDataSummary(item, true));
1395
+ }
1396
+ else if (item.type === "image_url.done") {
1397
+ lines.push(`Image URL: ${item.image_url}`);
1398
+ }
1399
+ else if (item.type === "inline_data.done") {
1400
+ lines.push(this._formatInlineDataSummary(item));
1401
+ }
1402
+ else if (item.type === "embedding.done") {
1403
+ lines.push(this._formatEmbeddingPreview(item));
1404
+ }
1405
+ else if (item.type === "tool_call.done") {
1406
+ lines.push(`Tool Call: ${item.name}`);
1407
+ lines.push(` Arguments: ${JSON.stringify(item.arguments, null, 2)}`);
1408
+ lines.push(` Tool Call ID: ${item.tool_call_id}`);
1409
+ }
1410
+ else if (item.type === "tool_result.done") {
1411
+ lines.push(`Tool Result (ID: ${item.tool_call_id}): ${item.text}`);
1412
+ if (item.images && item.images.length > 0) {
1413
+ item.images.forEach((imageUrl, i) => {
1414
+ lines.push(` Image ${i + 1}: ${imageUrl}`);
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ if (message.usage_metadata) {
1420
+ const metadata = message.usage_metadata;
1421
+ lines.push("\nUsage Metadata:");
1422
+ if (metadata.cached_tokens !== null) {
1423
+ lines.push(` Cached Tokens: ${metadata.cached_tokens}`);
1424
+ }
1425
+ if (metadata.prompt_tokens !== null) {
1426
+ lines.push(` Prompt Tokens: ${metadata.prompt_tokens}`);
1427
+ }
1428
+ if (metadata.thoughts_tokens !== null) {
1429
+ lines.push(` Thoughts Tokens: ${metadata.thoughts_tokens}`);
1430
+ }
1431
+ if (metadata.response_tokens !== null) {
1432
+ lines.push(` Response Tokens: ${metadata.response_tokens}`);
1433
+ }
1434
+ const inputTokens = (metadata.cached_tokens || 0) + (metadata.prompt_tokens || 0);
1435
+ const outputTokens = (metadata.thoughts_tokens || 0) + (metadata.response_tokens || 0);
1436
+ const totalTokens = inputTokens + outputTokens;
1437
+ lines.push(` Total Tokens: ${totalTokens}`);
1438
+ }
1439
+ if (message.finish_reason) {
1440
+ lines.push(`\nFinish Reason: ${message.finish_reason}`);
1441
+ }
1442
+ lines.push("");
1443
+ }
1444
+ return lines.join("\n");
1445
+ }
1446
+ /**
1447
+ * Create an Express web application for browsing conversation files.
1448
+ *
1449
+ * @returns Express application instance
1450
+ */
1451
+ createWebApp(options = {}) {
1452
+ const app = (0, express_1.default)();
1453
+ const basePath = this._normalizeBasePath(options.basePath);
1454
+ const rootUrl = this._prefixUrl(basePath, "/");
1455
+ const esc = (value) => this._escapeHtml(String(value));
1456
+ const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
1457
+ app.get("*", (req, res) => {
1458
+ const subpath = req.path.slice(1);
1459
+ const fullPath = path.resolve(path.join(this.cacheDir, subpath));
1460
+ if (!fullPath.startsWith(path.resolve(this.cacheDir))) {
1461
+ return res.status(403).send("Access denied");
1462
+ }
1463
+ if (!fs.existsSync(fullPath)) {
1464
+ return res.status(404).send("Path not found");
1465
+ }
1466
+ if (fs.statSync(fullPath).isFile()) {
1467
+ try {
1468
+ const parts = subpath ? subpath.split("/") : [];
1469
+ const breadcrumb = this._breadcrumb(basePath, parts);
1470
+ const backUrl = this._prefixUrl(basePath, parts.length > 1 ? "/" + parts.slice(0, -1).join("/") : "/");
1471
+ const filename = path.basename(fullPath);
1472
+ const pageHead = (meta) => `
1473
+ ${this._nav(breadcrumb, backUrl, true)}
1474
+ <div class="page-head">
1475
+ <div>
1476
+ <h1>${esc(filename)}</h1>
1477
+ <p class="page-meta">${meta}</p>
1478
+ </div>
1479
+ </div>`;
1480
+ if (!fullPath.endsWith(".json")) {
1481
+ const content = fs.readFileSync(fullPath, "utf-8");
1482
+ const body = `
1483
+ <main class="page">${pageHead("<span>Plain-text transcript</span>")}
1484
+ <pre class="card text-file">${esc(content)}</pre>
1485
+ </main>`;
1486
+ return res.send(this._page(`${filename} - MMSP Tracer`, rootUrl, body));
1487
+ }
1488
+ const data = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
1489
+ const config = data.config || {};
1490
+ // trace files written before 0.5.0 carry the legacy content item types
1491
+ const history = (0, legacy_1.normalizeLegacyMessages)(data.history || []);
1492
+ const totalRounds = Math.ceil(history.length / 2);
1493
+ // Python saves the time local and TypeScript in UTC; both show local
1494
+ const savedMs = Date.parse(data.timestamp || "");
1495
+ const savedAt = Number.isNaN(savedMs)
1496
+ ? String(data.timestamp || "")
1497
+ : this._formatTimestamp(savedMs);
1498
+ const metaParts = [];
1499
+ if (config.model) {
1500
+ metaParts.push(`<span class="mono"><b>${esc(config.model)}</b></span>`);
1501
+ }
1502
+ metaParts.push(`<span>${plural(history.length, "message", "messages")}</span>`);
1503
+ metaParts.push(`<span>${plural(totalRounds, "round", "rounds")}</span>`);
1504
+ if (savedAt) {
1505
+ metaParts.push(`<span>Saved ${esc(savedAt)}</span>`);
1506
+ }
1507
+ const configRows = Object.entries(config)
1508
+ .filter(([key]) => key !== "trace_id")
1509
+ .map(([key, value]) => {
1510
+ let valueHtml;
1511
+ if (key === "system_prompt" && value != null) {
1512
+ valueHtml = `<details class="fold"><summary>Show ${ICONS.chevron}</summary><pre>${esc(value)}</pre></details>`;
1513
+ }
1514
+ else if (key === "tools" && Array.isArray(value)) {
1515
+ valueHtml = `<details class="fold"><summary>${plural(value.length, "tool", "tools")} ${ICONS.chevron}</summary><pre>${esc(JSON.stringify(value, null, 2))}</pre></details>`;
1516
+ }
1517
+ else if (typeof value === "string") {
1518
+ valueHtml = esc(value);
1519
+ }
1520
+ else {
1521
+ valueHtml = esc(JSON.stringify(value));
1522
+ }
1523
+ return `
1524
+ <div class="kv-row"><dt>${esc(key)}</dt><dd>${valueHtml}</dd></div>`;
1525
+ })
1526
+ .join("");
1527
+ const configHtml = configRows
1528
+ ? `
1529
+ <section class="card config">
1530
+ <div class="card-head">Configuration</div>
1531
+ <dl class="kv">${configRows}
1532
+ </dl>
1533
+ </section>`
1534
+ : "";
1535
+ const messagesHtml = history
1536
+ .map((msg, idx) => {
1537
+ const itemsHtml = msg.content_items
1538
+ .map((item) => {
1539
+ let inner = "";
1540
+ if (item.type === "text.done") {
1541
+ inner = `<div class="item-text">${esc(item.text)}</div>`;
1542
+ }
1543
+ else if (item.type === "thinking.done") {
1544
+ inner = item.thinking.trim()
1545
+ ? `<div class="item-thinking">${esc(item.thinking.trim())}</div>`
1546
+ : `<div class="item-note">No text${item.fidelity ? ", only fidelity" : ""}</div>`;
1547
+ }
1548
+ else if (item.type === "inline_thinking.done") {
1549
+ inner = `<div class="item-note">${esc(this._formatInlineDataSummary(item, true))}</div>`;
1550
+ if (item.mime_type?.startsWith("image/")) {
1551
+ inner += `<img class="item-img" src="${esc(this._inlineDataUrl(item))}" alt="Thinking image">`;
1552
+ }
1553
+ }
1554
+ else if (item.type === "tool_call.done") {
1555
+ const args = Object.entries(item.arguments)
1556
+ .map(([key, value]) => `${esc(key)}="${esc(value)}"`)
1557
+ .join(", ");
1558
+ inner = `<div class="tool-sig">${esc(item.name)}(${args})</div>`;
1559
+ }
1560
+ else if (item.type === "tool_result.done") {
1561
+ const images = item.images && item.images.length > 0
1562
+ ? `<div class="item-images">${item.images
1563
+ .map((imageUrl) => `<img class="item-img" src="${esc(imageUrl)}" alt="Tool result image">`)
1564
+ .join("")}</div>`
1565
+ : "";
1566
+ inner = `<div class="result-text">${esc(item.text)}</div>${images}`;
1567
+ }
1568
+ else if (item.type === "image_url.done") {
1569
+ inner = `<img class="item-img" src="${esc(item.image_url)}" alt="Image">`;
1570
+ }
1571
+ else if (item.type === "inline_data.done") {
1572
+ inner = `<div class="item-note">${esc(this._formatInlineDataSummary(item))}</div>`;
1573
+ if (item.mime_type?.startsWith("image/")) {
1574
+ inner += `<img class="item-img" src="${esc(this._inlineDataUrl(item))}" alt="Inline image">`;
1575
+ }
1576
+ else if (this._inlineDataIsAudio(item)) {
1577
+ inner += `<audio class="item-audio" controls preload="metadata"><source src="${esc(this._inlineDataUrl(item))}" type="${esc(this._inlineDataAudioType(item))}"></audio>`;
1578
+ }
1579
+ }
1580
+ else if (item.type === "embedding.done") {
1581
+ inner = `<div class="embedding">${esc(this._formatEmbeddingPreview(item))}</div>`;
1582
+ }
1583
+ const callId = item.type === "tool_call.done" ||
1584
+ item.type === "tool_result.done"
1585
+ ? `<span class="item-id">${esc(item.tool_call_id)}</span>`
1586
+ : "";
1587
+ return `
1588
+ <div class="item kind-${esc(item.type.split(".")[0])}">
1589
+ <div class="item-head"><div class="item-type">${esc(item.type)}</div>${callId}</div>
1590
+ ${inner}
1591
+ </div>`;
1592
+ })
1593
+ .join("");
1594
+ let footHtml = "";
1595
+ if (msg.usage_metadata || msg.finish_reason) {
1596
+ footHtml = `
1597
+ <div class="msg-foot">`;
1598
+ if (msg.finish_reason) {
1599
+ footHtml += `<span class="reason reason-${esc(msg.finish_reason)}" title="Finish reason">${esc(msg.finish_reason)}</span>`;
1600
+ }
1601
+ const usage = msg.usage_metadata;
1602
+ if (usage) {
1603
+ const parts = [];
1604
+ const add = (label, value) => {
1605
+ if (value) {
1606
+ parts.push(`<span>${label} <b>${this._formatCount(value)}</b></span>`);
1607
+ }
1608
+ };
1609
+ add("Cached", usage.cached_tokens);
1610
+ add("Prompt", usage.prompt_tokens);
1611
+ add("Thoughts", usage.thoughts_tokens);
1612
+ add("Response", usage.response_tokens);
1613
+ const total = (usage.cached_tokens || 0) +
1614
+ (usage.prompt_tokens || 0) +
1615
+ (usage.thoughts_tokens || 0) +
1616
+ (usage.response_tokens || 0);
1617
+ parts.push(`<span>Total <b>${this._formatCount(total)}</b></span>`);
1618
+ footHtml += `<span class="usage" title="Token usage">${parts.join("")}</span>`;
1619
+ }
1620
+ footHtml += "</div>";
1621
+ }
1622
+ const prevMsg = idx > 0 ? history[idx - 1] : null;
1623
+ const tookHtml = prevMsg && msg.created_at && prevMsg.created_at
1624
+ ? `<span class="num" title="Time since the message before">${this._formatDuration(Math.abs(msg.created_at - prevMsg.created_at))}</span>`
1625
+ : "";
1626
+ const timestampHtml = msg.created_at
1627
+ ? `<span class="num">${this._formatTimestamp(msg.created_at)}</span>`
1628
+ : "";
1629
+ return `
1630
+ <details class="card msg-card" id="msg-${idx}" open>
1631
+ <summary class="msg-summary">
1632
+ <span class="role role-${esc(msg.role)}">${esc(msg.role)}</span>
1633
+ <span>${plural(msg.content_items.length, "item", "items")}</span>
1634
+ <span>Round ${Math.floor(idx / 2) + 1} / ${totalRounds}</span>
1635
+ <span class="spacer"></span>
1636
+ ${tookHtml}
1637
+ ${timestampHtml}
1638
+ ${ICONS.chevron}
1639
+ </summary>
1640
+ <div class="msg-body">${itemsHtml}${footHtml}
1641
+ </div>
1642
+ </details>`;
1643
+ })
1644
+ .join("");
1645
+ const body = `
1646
+ <main class="page">${pageHead(metaParts.join("\n"))}${configHtml}${messagesHtml}
1647
+ </main>
1648
+ ${this._buildSidebarHtml(totalRounds, history)}`;
1649
+ return res.send(this._page(`${filename} - MMSP Tracer`, rootUrl, body));
1650
+ }
1651
+ catch (error) {
1652
+ return res.status(500).send(`Error reading file: ${error}`);
1653
+ }
1654
+ }
1655
+ try {
1656
+ const sortBy = req.query["sort"] === "mtime" ? "mtime" : "name";
1657
+ const entries = fs
1658
+ .readdirSync(fullPath, { withFileTypes: true })
1659
+ .filter((entry) => entry.name !== ".DS_Store");
1660
+ const entryStats = new Map();
1661
+ for (const entry of entries) {
1662
+ entryStats.set(entry.name, fs.statSync(path.join(fullPath, entry.name)));
1663
+ }
1664
+ entries.sort((a, b) => {
1665
+ const aIsDir = a.isDirectory();
1666
+ const bIsDir = b.isDirectory();
1667
+ if (aIsDir !== bIsDir)
1668
+ return aIsDir ? -1 : 1;
1669
+ if (sortBy === "mtime") {
1670
+ const aMtime = entryStats.get(a.name).mtimeMs;
1671
+ const bMtime = entryStats.get(b.name).mtimeMs;
1672
+ return bMtime - aMtime;
1673
+ }
1674
+ return a.name.localeCompare(b.name);
1675
+ });
1676
+ const rowsHtml = entries
1677
+ .map((entry) => {
1678
+ const entryPath = path.join(fullPath, entry.name);
1679
+ const relativePath = path.relative(this.cacheDir, entryPath);
1680
+ const stat = entryStats.get(entry.name);
1681
+ const isDir = stat.isDirectory();
1682
+ let size = "";
1683
+ if (!isDir) {
1684
+ if (stat.size < 1024) {
1685
+ size = `${stat.size} B`;
1686
+ }
1687
+ else if (stat.size < 1024 * 1024) {
1688
+ size = `${(stat.size / 1024).toFixed(1)} KB`;
1689
+ }
1690
+ else {
1691
+ size = `${(stat.size / (1024 * 1024)).toFixed(1)} MB`;
1692
+ }
1693
+ }
1694
+ const url = this._prefixUrl(basePath, "/" + relativePath.replace(/\\/g, "/"));
1695
+ return `
1696
+ <a class="row" href="${esc(url)}">
1697
+ <span class="row-icon${isDir ? " dir" : ""}">${isDir ? ICONS.folder : ICONS.file}</span>
1698
+ <span class="row-name">${esc(entry.name)}</span>
1699
+ <span class="row-size">${size}</span>
1700
+ <span class="row-time">${this._formatTimestamp(stat.mtimeMs)}</span>
1701
+ </a>`;
1702
+ })
1703
+ .join("");
1704
+ const parts = subpath ? subpath.split("/").filter(Boolean) : [];
1705
+ const title = parts.length ? parts[parts.length - 1] : "Traces";
1706
+ const baseUrl = subpath ? "/" + subpath : "/";
1707
+ const sortNameUrl = this._prefixUrl(basePath, baseUrl + "?sort=name");
1708
+ const sortMtimeUrl = this._prefixUrl(basePath, baseUrl + "?sort=mtime");
1709
+ const current = (sort) => sortBy === sort ? ' aria-current="true"' : "";
1710
+ const body = `
1711
+ <main class="page">
1712
+ ${this._nav(this._breadcrumb(basePath, parts), parts.length
1713
+ ? this._prefixUrl(basePath, "/" + parts.slice(0, -1).join("/"))
1714
+ : undefined)}
1715
+ <div class="page-head">
1716
+ <div>
1717
+ <h1>${esc(title)}</h1>
1718
+ <p class="page-meta"><span>${plural(entries.length, "entry", "entries")}</span></p>
1719
+ </div>
1720
+ <div class="segmented" role="group" aria-label="Sort by">
1721
+ <a href="${esc(sortNameUrl)}"${current("name")}>Name</a>
1722
+ <a href="${esc(sortMtimeUrl)}"${current("mtime")}>Modified</a>
1723
+ </div>
1724
+ </div>
1725
+ <div class="card list">${rowsHtml ||
1726
+ `
1727
+ <div class="empty">No traces here yet. Requests with a <code>trace_id</code> in their config are saved here.</div>`}
1728
+ </div>
1729
+ </main>`;
1730
+ return res.send(this._page(`${title} - MMSP Tracer`, rootUrl, body));
1731
+ }
1732
+ catch (error) {
1733
+ return res.status(500).send(`Error listing directory: ${error}`);
1734
+ }
1735
+ });
1736
+ return app;
1737
+ }
1738
+ /**
1739
+ * Normalize the base path used when tracer is mounted inside another app.
1740
+ *
1741
+ * @param basePath - Optional URL prefix for tracer routes
1742
+ * @returns Normalized URL prefix without a trailing slash
1743
+ */
1744
+ _normalizeBasePath(basePath = "") {
1745
+ if (!basePath || basePath === "/") {
1746
+ return "";
1747
+ }
1748
+ const prefixed = basePath.startsWith("/") ? basePath : "/" + basePath;
1749
+ return prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
1750
+ }
1751
+ /**
1752
+ * Prefix an internal tracer URL with the mount path.
1753
+ *
1754
+ * @param basePath - Normalized tracer mount path
1755
+ * @param url - Internal URL beginning with /
1756
+ * @returns URL safe to render into tracer links
1757
+ */
1758
+ _prefixUrl(basePath, url) {
1759
+ if (!basePath) {
1760
+ return url;
1761
+ }
1762
+ if (url === "/") {
1763
+ return basePath + "/";
1764
+ }
1765
+ return basePath + url;
1766
+ }
1767
+ /**
1768
+ * Format a Unix timestamp in milliseconds as YYYY-MM-DD HH:MM:SS.
1769
+ *
1770
+ * @param ms - Unix timestamp in milliseconds
1771
+ * @returns Formatted date string
1772
+ */
1773
+ _formatTimestamp(ms) {
1774
+ const d = new Date(ms);
1775
+ const pad = (n) => n.toString().padStart(2, "0");
1776
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1777
+ }
1778
+ /**
1779
+ * Build the rail that lists the rounds of a trace.
1780
+ *
1781
+ * @param totalRounds - Total number of rounds
1782
+ * @param history - Full message history array
1783
+ * @returns HTML string for the rail
1784
+ */
1785
+ _buildSidebarHtml(totalRounds, history) {
1786
+ let links = "";
1787
+ for (let roundIdx = 0; roundIdx < totalRounds; roundIdx++) {
1788
+ let tookHtml = "";
1789
+ if (roundIdx > 0) {
1790
+ const currAssistant = history[roundIdx * 2 + 1];
1791
+ const prevAssistant = history[(roundIdx - 1) * 2 + 1];
1792
+ if (currAssistant?.created_at && prevAssistant?.created_at) {
1793
+ tookHtml = `<span>${this._formatDuration(Math.abs(currAssistant.created_at - prevAssistant.created_at))}</span>`;
1794
+ }
1795
+ }
1796
+ links += `
1797
+ <li><a href="#msg-${roundIdx * 2}">Round ${roundIdx + 1}${tookHtml}</a></li>`;
1798
+ }
1799
+ return `<aside class="rail" id="rail" aria-label="Rounds">
1800
+ <button type="button" class="rail-toggle" onclick="toggleRail()" aria-expanded="false">Round <span class="mono" id="railCurrent">1 / ${totalRounds}</span>${ICONS.chevron}</button>
1801
+ <div class="rail-panel">
1802
+ <div class="rail-title">Rounds (${totalRounds})</div>
1803
+ <ol class="rail-list">${links}
1804
+ </ol>
1805
+ </div>
1806
+ </aside>`;
1807
+ }
1808
+ /**
1809
+ * Format milliseconds the way the playground does: 840 ms, 2.41 s, 1 min 5 s.
1810
+ *
1811
+ * @param ms - Duration in milliseconds
1812
+ * @returns Formatted duration
1813
+ */
1814
+ _formatDuration(ms) {
1815
+ if (ms < 1000) {
1816
+ return `${Math.round(ms)} ms`;
1817
+ }
1818
+ if (ms < 60000) {
1819
+ return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)} s`;
1820
+ }
1821
+ return `${Math.floor(ms / 60000)} min ${Math.round((ms % 60000) / 1000)} s`;
1822
+ }
1823
+ /**
1824
+ * Group a count's digits by thousands.
1825
+ *
1826
+ * @param value - Count to format
1827
+ * @returns Formatted count
1828
+ */
1829
+ _formatCount(value) {
1830
+ return value.toLocaleString("en-US");
1831
+ }
1832
+ /**
1833
+ * Build the path from the cache root to the page, every step but the last a link.
1834
+ *
1835
+ * @param basePath - Normalized tracer mount path
1836
+ * @param parts - Path segments below the cache root
1837
+ * @returns Breadcrumb HTML
1838
+ */
1839
+ _breadcrumb(basePath, parts) {
1840
+ const steps = ["cache", ...parts];
1841
+ return steps
1842
+ .map((step, i) => {
1843
+ if (i === steps.length - 1) {
1844
+ return `<span class="crumb crumb-current">${this._escapeHtml(step)}</span>`;
1845
+ }
1846
+ const url = this._prefixUrl(basePath, "/" + parts.slice(0, i).join("/"));
1847
+ return `<a class="crumb" href="${this._escapeHtml(url)}">${this._escapeHtml(step)}</a>`;
1848
+ })
1849
+ .join(`<span class="crumb-sep">${ICONS.crumb}</span>`);
1850
+ }
1851
+ /**
1852
+ * Build the bar that opens a page, as in a file explorer: back, forward and up, then the address.
1853
+ *
1854
+ * @param breadcrumb - Breadcrumb HTML
1855
+ * @param upUrl - The parent the up button leads to, if there is one
1856
+ * @param isFile - Whether the page shows a file rather than a folder
1857
+ * @returns Navigation bar HTML
1858
+ */
1859
+ _nav(breadcrumb, upUrl, isFile = false) {
1860
+ const up = upUrl
1861
+ ? `<a class="nav-btn" href="${this._escapeHtml(upUrl)}" aria-label="Up" title="Up">${ICONS.arrow_up}</a>`
1862
+ : `<span class="nav-btn" aria-disabled="true" title="Up">${ICONS.arrow_up}</span>`;
1863
+ return ('<div class="page-nav"><div class="nav-buttons">' +
1864
+ '<button type="button" class="nav-btn" id="navBack" onclick="history.back()" aria-label="Back" ' +
1865
+ `title="Back">${ICONS.arrow_left}</button>` +
1866
+ '<button type="button" class="nav-btn forward" id="navForward" onclick="history.forward()" ' +
1867
+ `aria-label="Forward" title="Forward">${ICONS.arrow_right}</button>` +
1868
+ `${up}</div>` +
1869
+ `<nav class="address" aria-label="Path"><span class="address-icon">${isFile ? ICONS.file : ICONS.folder}` +
1870
+ `</span>${breadcrumb}</nav></div>`);
1871
+ }
1872
+ /**
1873
+ * Wrap a page body in the head, top bar and script every tracer page shares.
1874
+ *
1875
+ * @param title - Page title
1876
+ * @param rootUrl - URL of the cache root
1877
+ * @param body - Page body HTML
1878
+ * @returns Full HTML page
1879
+ */
1880
+ _page(title, rootUrl, body) {
1881
+ return ('<!DOCTYPE html>\n<html lang="en">\n<head>\n' +
1882
+ `<title>${this._escapeHtml(title)}</title>\n` +
1883
+ `${TRACER_HEAD}\n</head>\n<body>\n` +
1884
+ '<header class="topbar">' +
1885
+ `<a class="brand" href="${this._escapeHtml(rootUrl)}">${ICONS.mark}<span class="brand-name">MMSP</span>` +
1886
+ '<span class="brand-sub">Tracer</span></a>' +
1887
+ '<div class="topbar-actions">' +
1888
+ '<a href="https://github.com/Prism-Shadow/model-message-stream-protocol" target="_blank" ' +
1889
+ `rel="noopener noreferrer" class="ghost-btn" title="GitHub">${ICONS.github}` +
1890
+ '<span class="label-wide">GitHub</span></a>' +
1891
+ '<div class="segmented theme-toggle" id="themeToggle" role="radiogroup" aria-label="Theme">' +
1892
+ '<span class="seg-thumb" aria-hidden="true"></span>' +
1893
+ '<button type="button" role="radio" aria-checked="false" aria-label="Light theme" title="Light" ' +
1894
+ `data-theme-choice="light" onclick="setTheme('light')">${ICONS.sun}</button>` +
1895
+ '<button type="button" role="radio" aria-checked="false" aria-label="Dark theme" title="Dark" ' +
1896
+ `data-theme-choice="dark" onclick="setTheme('dark')">${ICONS.moon}</button>` +
1897
+ "</div></div></header>\n" +
1898
+ `${body}\n${TRACER_SCRIPT}\n</body>\n</html>\n`);
1899
+ }
1900
+ /**
1901
+ * Escape HTML special characters.
1902
+ *
1903
+ * @param text - Text to escape
1904
+ * @returns Escaped text
1905
+ */
1906
+ _escapeHtml(text) {
1907
+ return text
1908
+ .replace(/&/g, "&amp;")
1909
+ .replace(/</g, "&lt;")
1910
+ .replace(/>/g, "&gt;")
1911
+ .replace(/"/g, "&quot;")
1912
+ .replace(/'/g, "&#039;");
1913
+ }
1914
+ /**
1915
+ * Start the web server for browsing conversation files.
1916
+ *
1917
+ * @param host - Host address to bind to
1918
+ * @param port - Port number to listen on
1919
+ */
1920
+ startWebServer(host = "127.0.0.1", port = 25750) {
1921
+ const app = this.createWebApp();
1922
+ app.listen(port, host, () => {
1923
+ console.log(`Starting tracer web server at http://${host}:${port}`);
1924
+ console.log(`Cache directory: ${path.resolve(this.cacheDir)}`);
1925
+ });
1926
+ }
1927
+ }
1928
+ exports.Tracer = Tracer;