noctrace 0.6.1 → 0.7.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/README.md +6 -0
- package/dist/client/assets/index-BWO5fecq.css +2 -0
- package/dist/client/assets/index-BhUwV-5i.js +30 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/routes/api.js +78 -2
- package/dist/server/shared/filter.js +1 -1
- package/dist/server/shared/parser.js +249 -10
- package/dist/server/shared/token-cost.js +68 -0
- package/package.json +1 -1
- package/dist/client/assets/index-C4vi082v.css +0 -2
- package/dist/client/assets/index-Cm74Eldg.js +0 -30
package/dist/client/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BhUwV-5i.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BWO5fecq.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
|
@@ -6,7 +6,7 @@ import { Router } from 'express';
|
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { WebSocket } from 'ws';
|
|
9
|
-
import { parseJsonlContent, parseCompactionBoundaries, extractSessionId, extractSessionTitle, extractAgentIds, parseSubAgentContent, } from '../../shared/parser.js';
|
|
9
|
+
import { parseJsonlContent, parseCompactionBoundaries, extractSessionId, extractSessionTitle, extractAgentIds, parseSubAgentContent, parseInstructionsLoaded, } from '../../shared/parser.js';
|
|
10
10
|
import { computeContextHealth } from '../../shared/health.js';
|
|
11
11
|
import { parseAssistantTurns, computeDrift } from '../../shared/drift.js';
|
|
12
12
|
import { attachEfficiencyTips } from '../../shared/tips.js';
|
|
@@ -66,6 +66,8 @@ function assertWithinBase(resolved, base) {
|
|
|
66
66
|
export function buildApiRouter(claudeHome, wss) {
|
|
67
67
|
const router = Router();
|
|
68
68
|
const projectsDir = path.join(claudeHome, 'projects');
|
|
69
|
+
const teamsDir = path.join(claudeHome, 'teams');
|
|
70
|
+
const tasksDir = path.join(claudeHome, 'tasks');
|
|
69
71
|
/**
|
|
70
72
|
* In-memory registry of MCP-registered session paths.
|
|
71
73
|
* Populated by POST /api/sessions/register; cleared on unregister or server restart.
|
|
@@ -167,6 +169,79 @@ export function buildApiRouter(claudeHome, wss) {
|
|
|
167
169
|
}
|
|
168
170
|
});
|
|
169
171
|
// ---------------------------------------------------------------------------
|
|
172
|
+
// GET /api/teams
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Scan ~/.claude/teams/ and return an array of AgentTeam objects.
|
|
176
|
+
* Returns an empty array if the teams directory doesn't exist.
|
|
177
|
+
* Each team includes its members (from config.json) and taskCount.
|
|
178
|
+
*/
|
|
179
|
+
router.get('/teams', async (_req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
let teamDirs;
|
|
182
|
+
try {
|
|
183
|
+
teamDirs = await fs.readdir(teamsDir);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Teams directory doesn't exist — this is normal when the feature is unused
|
|
187
|
+
res.json([]);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const teams = [];
|
|
191
|
+
for (const teamName of teamDirs) {
|
|
192
|
+
const teamPath = path.join(teamsDir, teamName);
|
|
193
|
+
let stat;
|
|
194
|
+
try {
|
|
195
|
+
stat = await fs.stat(teamPath);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!stat.isDirectory())
|
|
201
|
+
continue;
|
|
202
|
+
// Read team config.json
|
|
203
|
+
const configPath = path.join(teamPath, 'config.json');
|
|
204
|
+
let members = [];
|
|
205
|
+
try {
|
|
206
|
+
const configRaw = await fs.readFile(configPath, 'utf8');
|
|
207
|
+
const config = JSON.parse(configRaw);
|
|
208
|
+
const rawMembers = Array.isArray(config['members']) ? config['members'] : [];
|
|
209
|
+
for (const m of rawMembers) {
|
|
210
|
+
if (typeof m !== 'object' || m === null)
|
|
211
|
+
continue;
|
|
212
|
+
const member = m;
|
|
213
|
+
members.push({
|
|
214
|
+
name: typeof member['name'] === 'string' ? member['name'] : 'Unknown',
|
|
215
|
+
agentId: typeof member['agent_id'] === 'string' ? member['agent_id']
|
|
216
|
+
: typeof member['agentId'] === 'string' ? member['agentId'] : '',
|
|
217
|
+
agentType: typeof member['agent_type'] === 'string' ? member['agent_type']
|
|
218
|
+
: typeof member['agentType'] === 'string' ? member['agentType'] : '',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// config.json missing or malformed — include team with empty members
|
|
224
|
+
}
|
|
225
|
+
// Count task files in ~/.claude/tasks/{team-name}/
|
|
226
|
+
let taskCount = 0;
|
|
227
|
+
const teamTasksDir = path.join(tasksDir, teamName);
|
|
228
|
+
try {
|
|
229
|
+
const taskFiles = await fs.readdir(teamTasksDir);
|
|
230
|
+
taskCount = taskFiles.length;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Task directory doesn't exist — taskCount stays 0
|
|
234
|
+
}
|
|
235
|
+
teams.push({ name: teamName, members, taskCount });
|
|
236
|
+
}
|
|
237
|
+
res.json(teams);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
241
|
+
res.status(500).json({ error: message });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
170
245
|
// GET /api/sessions/:slug
|
|
171
246
|
// ---------------------------------------------------------------------------
|
|
172
247
|
/**
|
|
@@ -315,6 +390,7 @@ export function buildApiRouter(claudeHome, wss) {
|
|
|
315
390
|
const sessionId = extractSessionId(content) ?? id;
|
|
316
391
|
const turns = parseAssistantTurns(content);
|
|
317
392
|
const drift = computeDrift(turns);
|
|
393
|
+
const instructionsLoaded = parseInstructionsLoaded(content);
|
|
318
394
|
// Load sub-agent JSONL files and attach as children to matching agent rows
|
|
319
395
|
const subagentsDir = path.join(projectsDir, slug, id, 'subagents');
|
|
320
396
|
let subagentsDirExists = false;
|
|
@@ -377,7 +453,7 @@ export function buildApiRouter(claudeHome, wss) {
|
|
|
377
453
|
return r.reduce((sum, row) => sum + row.tips.length + countTips(row.children), 0);
|
|
378
454
|
}
|
|
379
455
|
const tipCount = countTips(rows);
|
|
380
|
-
res.json({ rows, compactionBoundaries: boundaries, health, sessionId, drift, tipCount });
|
|
456
|
+
res.json({ rows, compactionBoundaries: boundaries, health, sessionId, drift, tipCount, instructionsLoaded });
|
|
381
457
|
}
|
|
382
458
|
catch (err) {
|
|
383
459
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -139,7 +139,7 @@ function rowMatchesDirect(row, parsed) {
|
|
|
139
139
|
// Type filter (OR among typeFilters)
|
|
140
140
|
if (parsed.typeFilters.length > 0) {
|
|
141
141
|
const rowTypeLower = row.toolName.toLowerCase();
|
|
142
|
-
const rowKind = row.type; // 'agent' | 'tool'
|
|
142
|
+
const rowKind = row.type; // 'agent' | 'tool' | 'api-error'
|
|
143
143
|
const matched = parsed.typeFilters.some((tf) => rowTypeLower === tf || rowTypeLower.includes(tf) || rowKind === tf);
|
|
144
144
|
if (!matched)
|
|
145
145
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPricing, computeCost } from './token-cost.js';
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// Type guards and helpers
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -70,6 +71,40 @@ function parseLine(line, idx) {
|
|
|
70
71
|
function isAgent(name) {
|
|
71
72
|
return name === 'Agent' || name === 'Task';
|
|
72
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns true when a tool result with is_error=true represents a tool execution
|
|
76
|
+
* failure (crash, timeout, permission denied) rather than a tool that ran and
|
|
77
|
+
* returned an error value. We distinguish by looking for crash/failure keywords
|
|
78
|
+
* in the output text.
|
|
79
|
+
*/
|
|
80
|
+
function isToolFailure(output) {
|
|
81
|
+
const lower = output.toLowerCase();
|
|
82
|
+
return (lower.includes('process exited with') ||
|
|
83
|
+
lower.includes('timed out') ||
|
|
84
|
+
lower.includes('timeout') ||
|
|
85
|
+
lower.includes('permission denied') ||
|
|
86
|
+
lower.includes('killed') ||
|
|
87
|
+
lower.includes('segmentation fault') ||
|
|
88
|
+
lower.includes('posttoolusedfailure') ||
|
|
89
|
+
lower.includes('tool execution failed') ||
|
|
90
|
+
lower.includes('failed to execute'));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Classify an API stop failure message into a short error class label.
|
|
94
|
+
* Returns e.g. "Rate Limit", "Billing Error", "Server Error", "Auth Error".
|
|
95
|
+
*/
|
|
96
|
+
function classifyStopFailure(message) {
|
|
97
|
+
const lower = message.toLowerCase();
|
|
98
|
+
if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('429'))
|
|
99
|
+
return 'Rate Limit';
|
|
100
|
+
if (lower.includes('billing') || lower.includes('payment') || lower.includes('credit'))
|
|
101
|
+
return 'Billing Error';
|
|
102
|
+
if (lower.includes('auth') || lower.includes('unauthorized') || lower.includes('403') || lower.includes('401'))
|
|
103
|
+
return 'Auth Error';
|
|
104
|
+
if (lower.includes('overloaded') || lower.includes('529'))
|
|
105
|
+
return 'Overloaded';
|
|
106
|
+
return 'Server Error';
|
|
107
|
+
}
|
|
73
108
|
// ---------------------------------------------------------------------------
|
|
74
109
|
// Public API
|
|
75
110
|
// ---------------------------------------------------------------------------
|
|
@@ -100,11 +135,15 @@ export function parseJsonlContent(content) {
|
|
|
100
135
|
for (const block of c) {
|
|
101
136
|
if (isObj(block) && block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string') {
|
|
102
137
|
const tb = block;
|
|
138
|
+
const output = extractContent(tb.content);
|
|
139
|
+
const isError = tb.is_error === true;
|
|
103
140
|
resultMap.set(tb.tool_use_id, {
|
|
104
141
|
endTime,
|
|
105
|
-
output
|
|
106
|
-
isError
|
|
142
|
+
output,
|
|
143
|
+
isError,
|
|
144
|
+
isFailure: isError && isToolFailure(output),
|
|
107
145
|
totalDurationMs: ur.toolUseResult?.totalDurationMs,
|
|
146
|
+
agentType: ur.toolUseResult?.agentType,
|
|
108
147
|
});
|
|
109
148
|
}
|
|
110
149
|
}
|
|
@@ -199,10 +238,12 @@ export function parseJsonlContent(content) {
|
|
|
199
238
|
const ar = rec;
|
|
200
239
|
const startTime = new Date(ar.timestamp).getTime();
|
|
201
240
|
const usage = ar.message.usage;
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
const cacheReadTokens = usage?.cache_read_input_tokens ?? 0;
|
|
242
|
+
const cacheCreateTokens = usage?.cache_creation_input_tokens ?? 0;
|
|
243
|
+
const rawInputTokens = usage?.input_tokens ?? 0;
|
|
244
|
+
const inputTokens = rawInputTokens + cacheCreateTokens + cacheReadTokens;
|
|
205
245
|
const outputTokens = usage?.output_tokens ?? 0;
|
|
246
|
+
const modelName = typeof ar.message.model === 'string' ? ar.message.model : null;
|
|
206
247
|
const contextFillPercent = 0; // recalculated after peak detection
|
|
207
248
|
for (const block of ar.message.content) {
|
|
208
249
|
if (!isToolUse(block))
|
|
@@ -234,6 +275,9 @@ export function parseJsonlContent(content) {
|
|
|
234
275
|
output: res ? res.output : null,
|
|
235
276
|
inputTokens,
|
|
236
277
|
outputTokens,
|
|
278
|
+
cacheReadTokens,
|
|
279
|
+
cacheCreateTokens,
|
|
280
|
+
modelName,
|
|
237
281
|
contextFillPercent,
|
|
238
282
|
isReread,
|
|
239
283
|
assistantUuid: ar.uuid,
|
|
@@ -275,6 +319,12 @@ export function parseJsonlContent(content) {
|
|
|
275
319
|
// First pass: create rows from top-level assistant records
|
|
276
320
|
const rowById = new Map();
|
|
277
321
|
for (const p of pending) {
|
|
322
|
+
const pricing = getPricing(p.modelName);
|
|
323
|
+
const rawInput = Math.max(0, p.inputTokens - p.cacheReadTokens - p.cacheCreateTokens);
|
|
324
|
+
const estimatedCost = p.inputTokens > 0 || p.outputTokens > 0
|
|
325
|
+
? computeCost(pricing, rawInput, p.outputTokens, p.cacheReadTokens, p.cacheCreateTokens)
|
|
326
|
+
: null;
|
|
327
|
+
const res = resultMap.get(p.id);
|
|
278
328
|
rowById.set(p.id, {
|
|
279
329
|
id: p.id,
|
|
280
330
|
type: isAgent(p.toolName) ? 'agent' : 'tool',
|
|
@@ -292,8 +342,12 @@ export function parseJsonlContent(content) {
|
|
|
292
342
|
tokenDelta: 0,
|
|
293
343
|
contextFillPercent: p.contextFillPercent,
|
|
294
344
|
isReread: p.isReread,
|
|
345
|
+
isFailure: res?.isFailure ?? false,
|
|
295
346
|
children: [],
|
|
296
347
|
tips: [],
|
|
348
|
+
modelName: p.modelName,
|
|
349
|
+
estimatedCost,
|
|
350
|
+
agentType: res?.agentType ?? null,
|
|
297
351
|
});
|
|
298
352
|
}
|
|
299
353
|
// Create rows from sub-agent progress records
|
|
@@ -326,8 +380,12 @@ export function parseJsonlContent(content) {
|
|
|
326
380
|
tokenDelta: 0,
|
|
327
381
|
contextFillPercent: ctxFill,
|
|
328
382
|
isReread,
|
|
383
|
+
isFailure: res ? (res.isError && isToolFailure(res.output)) : false,
|
|
329
384
|
children: [],
|
|
330
385
|
tips: [],
|
|
386
|
+
modelName: null,
|
|
387
|
+
estimatedCost: null,
|
|
388
|
+
agentType: null,
|
|
331
389
|
});
|
|
332
390
|
}
|
|
333
391
|
// Nest children: top-level rows use parentUuid ancestry, sub-agent rows use parentToolUseID
|
|
@@ -404,6 +462,52 @@ export function parseJsonlContent(content) {
|
|
|
404
462
|
}
|
|
405
463
|
}
|
|
406
464
|
computeDeltas(top);
|
|
465
|
+
// Detect API stop failures from system records (rate limit, billing, auth, server errors).
|
|
466
|
+
// These appear as system records with subtype 'stop_failure' or similar.
|
|
467
|
+
// We insert them as top-level 'api-error' rows at their point in time.
|
|
468
|
+
for (const rec of records) {
|
|
469
|
+
if (rec.type !== 'system')
|
|
470
|
+
continue;
|
|
471
|
+
const sr = rec;
|
|
472
|
+
const subtype = sr.subtype ?? '';
|
|
473
|
+
const isStopFailure = subtype === 'stop_failure' ||
|
|
474
|
+
subtype === 'api_error' ||
|
|
475
|
+
subtype === 'request_failed';
|
|
476
|
+
if (!isStopFailure)
|
|
477
|
+
continue;
|
|
478
|
+
const ts = new Date(sr.timestamp).getTime();
|
|
479
|
+
// Try to extract an error message from the record's top-level fields.
|
|
480
|
+
const raw = rec;
|
|
481
|
+
const errorMsg = (typeof raw['error'] === 'string' ? raw['error'] : null) ||
|
|
482
|
+
(typeof raw['message'] === 'string' ? raw['message'] : null) ||
|
|
483
|
+
subtype;
|
|
484
|
+
const errorClass = classifyStopFailure(errorMsg);
|
|
485
|
+
const rowId = `api-error-${sr.uuid}`;
|
|
486
|
+
top.push({
|
|
487
|
+
id: rowId,
|
|
488
|
+
type: 'api-error',
|
|
489
|
+
toolName: errorClass,
|
|
490
|
+
label: errorMsg,
|
|
491
|
+
startTime: ts,
|
|
492
|
+
endTime: ts,
|
|
493
|
+
duration: 0,
|
|
494
|
+
status: 'error',
|
|
495
|
+
parentAgentId: null,
|
|
496
|
+
input: {},
|
|
497
|
+
output: errorMsg,
|
|
498
|
+
inputTokens: 0,
|
|
499
|
+
outputTokens: 0,
|
|
500
|
+
tokenDelta: 0,
|
|
501
|
+
contextFillPercent: 0,
|
|
502
|
+
isReread: false,
|
|
503
|
+
isFailure: false,
|
|
504
|
+
children: [],
|
|
505
|
+
tips: [],
|
|
506
|
+
modelName: null,
|
|
507
|
+
estimatedCost: null,
|
|
508
|
+
agentType: null,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
407
511
|
return top;
|
|
408
512
|
}
|
|
409
513
|
/**
|
|
@@ -487,10 +591,13 @@ export function parseSubAgentContent(content) {
|
|
|
487
591
|
for (const block of c) {
|
|
488
592
|
if (isObj(block) && block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string') {
|
|
489
593
|
const tb = block;
|
|
594
|
+
const output = extractContent(tb.content);
|
|
595
|
+
const isError = tb.is_error === true;
|
|
490
596
|
resultMap.set(tb.tool_use_id, {
|
|
491
597
|
endTime,
|
|
492
|
-
output
|
|
493
|
-
isError
|
|
598
|
+
output,
|
|
599
|
+
isError,
|
|
600
|
+
isFailure: isError && isToolFailure(output),
|
|
494
601
|
});
|
|
495
602
|
}
|
|
496
603
|
}
|
|
@@ -517,11 +624,15 @@ export function parseSubAgentContent(content) {
|
|
|
517
624
|
const ar = rec;
|
|
518
625
|
const startTime = new Date(ar.timestamp).getTime();
|
|
519
626
|
const usage = ar.message.usage;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
627
|
+
const cacheReadTokens = usage?.cache_read_input_tokens ?? 0;
|
|
628
|
+
const cacheCreateTokens = usage?.cache_creation_input_tokens ?? 0;
|
|
629
|
+
const rawInputTokens = usage?.input_tokens ?? 0;
|
|
630
|
+
const inputTokens = rawInputTokens + cacheCreateTokens + cacheReadTokens;
|
|
523
631
|
const outputTokens = usage?.output_tokens ?? 0;
|
|
632
|
+
const modelName = typeof ar.message.model === 'string' ? ar.message.model : null;
|
|
524
633
|
const contextFillPercent = (inputTokens / effectiveWindow) * 100;
|
|
634
|
+
const pricing = getPricing(modelName);
|
|
635
|
+
const rawInput = Math.max(0, rawInputTokens);
|
|
525
636
|
for (const block of ar.message.content) {
|
|
526
637
|
if (!isToolUse(block))
|
|
527
638
|
continue;
|
|
@@ -530,6 +641,9 @@ export function parseSubAgentContent(content) {
|
|
|
530
641
|
const isReread = fp !== null && seenPaths.has(fp);
|
|
531
642
|
if (fp !== null)
|
|
532
643
|
seenPaths.add(fp);
|
|
644
|
+
const estimatedCost = inputTokens > 0 || outputTokens > 0
|
|
645
|
+
? computeCost(pricing, rawInput, outputTokens, cacheReadTokens, cacheCreateTokens)
|
|
646
|
+
: null;
|
|
533
647
|
const res = resultMap.get(block.id);
|
|
534
648
|
rows.push({
|
|
535
649
|
id: block.id,
|
|
@@ -548,11 +662,57 @@ export function parseSubAgentContent(content) {
|
|
|
548
662
|
tokenDelta: 0,
|
|
549
663
|
contextFillPercent,
|
|
550
664
|
isReread,
|
|
665
|
+
isFailure: res?.isFailure ?? false,
|
|
551
666
|
children: [],
|
|
552
667
|
tips: [],
|
|
668
|
+
modelName,
|
|
669
|
+
estimatedCost,
|
|
670
|
+
agentType: null,
|
|
553
671
|
});
|
|
554
672
|
}
|
|
555
673
|
}
|
|
674
|
+
// Detect API stop failures within sub-agent content
|
|
675
|
+
for (const rec of records) {
|
|
676
|
+
if (rec.type !== 'system')
|
|
677
|
+
continue;
|
|
678
|
+
const sr = rec;
|
|
679
|
+
const subtype = sr.subtype ?? '';
|
|
680
|
+
const isStopFailure = subtype === 'stop_failure' ||
|
|
681
|
+
subtype === 'api_error' ||
|
|
682
|
+
subtype === 'request_failed';
|
|
683
|
+
if (!isStopFailure)
|
|
684
|
+
continue;
|
|
685
|
+
const ts = new Date(sr.timestamp).getTime();
|
|
686
|
+
const raw = rec;
|
|
687
|
+
const errorMsg = (typeof raw['error'] === 'string' ? raw['error'] : null) ||
|
|
688
|
+
(typeof raw['message'] === 'string' ? raw['message'] : null) ||
|
|
689
|
+
subtype;
|
|
690
|
+
const errorClass = classifyStopFailure(errorMsg);
|
|
691
|
+
rows.push({
|
|
692
|
+
id: `api-error-${sr.uuid}`,
|
|
693
|
+
type: 'api-error',
|
|
694
|
+
toolName: errorClass,
|
|
695
|
+
label: errorMsg,
|
|
696
|
+
startTime: ts,
|
|
697
|
+
endTime: ts,
|
|
698
|
+
duration: 0,
|
|
699
|
+
status: 'error',
|
|
700
|
+
parentAgentId: null,
|
|
701
|
+
input: {},
|
|
702
|
+
output: errorMsg,
|
|
703
|
+
inputTokens: 0,
|
|
704
|
+
outputTokens: 0,
|
|
705
|
+
tokenDelta: 0,
|
|
706
|
+
contextFillPercent: 0,
|
|
707
|
+
isReread: false,
|
|
708
|
+
isFailure: false,
|
|
709
|
+
children: [],
|
|
710
|
+
tips: [],
|
|
711
|
+
modelName: null,
|
|
712
|
+
estimatedCost: null,
|
|
713
|
+
agentType: null,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
556
716
|
// Compute per-row token delta for sub-agent rows
|
|
557
717
|
const sorted = [...rows].sort((a, b) => a.startTime - b.startTime);
|
|
558
718
|
let prevInput = 0;
|
|
@@ -563,6 +723,85 @@ export function parseSubAgentContent(content) {
|
|
|
563
723
|
}
|
|
564
724
|
return rows;
|
|
565
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Parse instruction-loading records from JSONL content.
|
|
728
|
+
* Looks for system records with subtype containing "instructions" or similar patterns,
|
|
729
|
+
* as well as user records with isMeta=true that describe loaded CLAUDE.md files.
|
|
730
|
+
* Returns a deduplicated list of InstructionFile entries. Never throws.
|
|
731
|
+
*/
|
|
732
|
+
export function parseInstructionsLoaded(content) {
|
|
733
|
+
const lines = content.split('\n');
|
|
734
|
+
const seen = new Set();
|
|
735
|
+
const result = [];
|
|
736
|
+
for (let i = 0; i < lines.length; i++) {
|
|
737
|
+
const line = lines[i].trim();
|
|
738
|
+
if (!line)
|
|
739
|
+
continue;
|
|
740
|
+
let parsed;
|
|
741
|
+
try {
|
|
742
|
+
parsed = JSON.parse(line);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (!isObj(parsed))
|
|
748
|
+
continue;
|
|
749
|
+
const recordType = parsed['type'];
|
|
750
|
+
const subtype = typeof parsed['subtype'] === 'string' ? parsed['subtype'] : null;
|
|
751
|
+
// Match system records that describe loaded instruction files
|
|
752
|
+
if (recordType === 'system') {
|
|
753
|
+
// Claude Code may emit system records with subtype: "instructions_loaded",
|
|
754
|
+
// "claude_md_loaded", "context_loaded", or similar
|
|
755
|
+
const isInstructionRecord = subtype !== null && (subtype.includes('instruction') ||
|
|
756
|
+
subtype.includes('claude_md') ||
|
|
757
|
+
subtype.includes('context_load') ||
|
|
758
|
+
subtype.includes('system_prompt'));
|
|
759
|
+
if (isInstructionRecord) {
|
|
760
|
+
const filePath = typeof parsed['filePath'] === 'string' ? parsed['filePath']
|
|
761
|
+
: typeof parsed['file_path'] === 'string' ? parsed['file_path']
|
|
762
|
+
: typeof parsed['path'] === 'string' ? parsed['path']
|
|
763
|
+
: null;
|
|
764
|
+
if (!filePath || seen.has(filePath))
|
|
765
|
+
continue;
|
|
766
|
+
seen.add(filePath);
|
|
767
|
+
const loadReason = typeof parsed['reason'] === 'string' ? parsed['reason']
|
|
768
|
+
: typeof parsed['load_reason'] === 'string' ? parsed['load_reason']
|
|
769
|
+
: subtype ?? 'session_start';
|
|
770
|
+
const estimatedTokens = typeof parsed['tokens'] === 'number' ? parsed['tokens']
|
|
771
|
+
: typeof parsed['token_count'] === 'number' ? parsed['token_count']
|
|
772
|
+
: null;
|
|
773
|
+
const parentFilePath = typeof parsed['parentFilePath'] === 'string' ? parsed['parentFilePath']
|
|
774
|
+
: typeof parsed['parent_file_path'] === 'string' ? parsed['parent_file_path']
|
|
775
|
+
: null;
|
|
776
|
+
result.push({ filePath, loadReason, estimatedTokens, parentFilePath });
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// Match user records with isMeta=true that list loaded context files
|
|
781
|
+
if (recordType === 'user' && parsed['isMeta'] === true) {
|
|
782
|
+
const metaContent = parsed['content'];
|
|
783
|
+
const contentStr = typeof metaContent === 'string' ? metaContent : '';
|
|
784
|
+
if (!contentStr)
|
|
785
|
+
continue;
|
|
786
|
+
// Look for patterns like "Loaded CLAUDE.md from /path/to/CLAUDE.md"
|
|
787
|
+
// or "Instructions loaded: /path/to/CLAUDE.md"
|
|
788
|
+
const filePathMatches = contentStr.matchAll(/(?:loaded|reading|including)[:\s]+([^\s,\n]+\.md)/gi);
|
|
789
|
+
for (const match of filePathMatches) {
|
|
790
|
+
const filePath = match[1];
|
|
791
|
+
if (!filePath || seen.has(filePath))
|
|
792
|
+
continue;
|
|
793
|
+
seen.add(filePath);
|
|
794
|
+
result.push({
|
|
795
|
+
filePath,
|
|
796
|
+
loadReason: 'session_start',
|
|
797
|
+
estimatedTokens: null,
|
|
798
|
+
parentFilePath: null,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
566
805
|
/**
|
|
567
806
|
* Extract a mapping of Agent/Task tool_use IDs to sub-agent IDs from session content.
|
|
568
807
|
* Returns a Map where keys are the parent tool_use.id values and values are agentId strings
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token cost estimation utilities for Claude models.
|
|
3
|
+
* Pricing is based on Claude's public API pricing (USD per million tokens).
|
|
4
|
+
*/
|
|
5
|
+
/** Sonnet pricing (default) */
|
|
6
|
+
const SONNET_PRICING = {
|
|
7
|
+
inputPerMTok: 3.00,
|
|
8
|
+
outputPerMTok: 15.00,
|
|
9
|
+
cacheReadPerMTok: 0.30,
|
|
10
|
+
cacheCreatePerMTok: 3.75,
|
|
11
|
+
};
|
|
12
|
+
/** Opus pricing */
|
|
13
|
+
const OPUS_PRICING = {
|
|
14
|
+
inputPerMTok: 15.00,
|
|
15
|
+
outputPerMTok: 75.00,
|
|
16
|
+
cacheReadPerMTok: 1.50,
|
|
17
|
+
cacheCreatePerMTok: 18.75,
|
|
18
|
+
};
|
|
19
|
+
/** Haiku pricing */
|
|
20
|
+
const HAIKU_PRICING = {
|
|
21
|
+
inputPerMTok: 0.80,
|
|
22
|
+
outputPerMTok: 4.00,
|
|
23
|
+
cacheReadPerMTok: 0.08,
|
|
24
|
+
cacheCreatePerMTok: 1.00,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Returns pricing for a given model name.
|
|
28
|
+
* Defaults to Sonnet pricing when the model is null or unrecognized.
|
|
29
|
+
*/
|
|
30
|
+
export function getPricing(modelName) {
|
|
31
|
+
if (!modelName)
|
|
32
|
+
return SONNET_PRICING;
|
|
33
|
+
const lower = modelName.toLowerCase();
|
|
34
|
+
if (lower.includes('opus'))
|
|
35
|
+
return OPUS_PRICING;
|
|
36
|
+
if (lower.includes('haiku'))
|
|
37
|
+
return HAIKU_PRICING;
|
|
38
|
+
// Sonnet and any unknown model default to Sonnet pricing
|
|
39
|
+
return SONNET_PRICING;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Computes the estimated cost in USD for a single assistant turn.
|
|
43
|
+
*
|
|
44
|
+
* @param pricing - The per-MTok pricing for the model
|
|
45
|
+
* @param input - Raw input tokens (non-cached)
|
|
46
|
+
* @param output - Output tokens
|
|
47
|
+
* @param cacheRead - Cache read tokens (default 0)
|
|
48
|
+
* @param cacheCreate - Cache creation tokens (default 0)
|
|
49
|
+
* @returns Estimated cost in USD
|
|
50
|
+
*/
|
|
51
|
+
export function computeCost(pricing, input, output, cacheRead = 0, cacheCreate = 0) {
|
|
52
|
+
const M = 1_000_000;
|
|
53
|
+
return ((input * pricing.inputPerMTok) / M +
|
|
54
|
+
(output * pricing.outputPerMTok) / M +
|
|
55
|
+
(cacheRead * pricing.cacheReadPerMTok) / M +
|
|
56
|
+
(cacheCreate * pricing.cacheCreatePerMTok) / M);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Formats a cost value in USD to a human-readable string.
|
|
60
|
+
* Uses fixed 4 decimal places for very small values, 2 for larger ones.
|
|
61
|
+
*/
|
|
62
|
+
export function formatCost(cost) {
|
|
63
|
+
if (cost >= 1)
|
|
64
|
+
return `$${cost.toFixed(2)}`;
|
|
65
|
+
if (cost >= 0.01)
|
|
66
|
+
return `$${cost.toFixed(3)}`;
|
|
67
|
+
return `$${cost.toFixed(4)}`;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--radius-sm:.25rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-8{top:calc(var(--spacing) * 8)}.right-0{right:calc(var(--spacing) * 0)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.right-2{right:calc(var(--spacing) * 2)}.left-0{left:calc(var(--spacing) * 0)}.left-2{left:calc(var(--spacing) * 2)}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-full{height:100%}.w-full{width:100%}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-sm{border-radius:var(--radius-sm)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-7{padding-left:calc(var(--spacing) * 7)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.uppercase{text-transform:uppercase}.italic{font-style:italic}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-none{-webkit-user-select:none;user-select:none}.\[file\:line\]{file:line}@media (width>=48rem){.md\:hidden{display:none}}}:root{--ctp-base:#1e1e2e;--ctp-mantle:#181825;--ctp-crust:#11111b;--ctp-surface0:#313244;--ctp-surface1:#45475a;--ctp-surface2:#585b70;--ctp-overlay0:#6c7086;--ctp-overlay1:#7f849c;--ctp-text:#cdd6f4;--ctp-subtext0:#a6adc8;--ctp-subtext1:#bac2de;--ctp-blue:#89b4fa;--ctp-green:#a6e3a1;--ctp-yellow:#f9e2af;--ctp-peach:#fab387;--ctp-mauve:#cba6f7;--ctp-teal:#94e2d5;--ctp-red:#f38ba8;--ctp-pink:#f5c2e7;--ctp-lavender:#b4befe;--color-read:var(--ctp-blue);--color-write:var(--ctp-green);--color-edit:var(--ctp-yellow);--color-bash:var(--ctp-peach);--color-agent:var(--ctp-mauve);--color-grep:var(--ctp-teal);--color-error:var(--ctp-red);--color-running:var(--ctp-pink)}body{background-color:var(--ctp-base);color:var(--ctp-text);font-family:SF Mono,Cascadia Code,JetBrains Mono,Fira Code,ui-monospace,monospace}@keyframes pulse-edge{0%,to{opacity:.6}50%{opacity:.1}}.running-pulse{animation:1.2s ease-in-out infinite pulse-edge}@keyframes noc-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}@keyframes noc-blink{0%,to{opacity:1}50%{opacity:0}}@media (width<=768px){.hidden-mobile{display:none!important}}.sidebar-panel{background-color:var(--ctp-mantle);flex-direction:column;flex-shrink:0;width:240px;display:flex;overflow:hidden}@media (width<=767px){.sidebar-panel{width:240px;transition:transform .2s;position:fixed;top:0;bottom:0;left:0;transform:translate(-100%)}.sidebar-panel[data-open=true]{transform:translate(0)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
|