pulse-line 1.0.3 → 1.0.5
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/.claude-plugin/plugin.json +1 -0
- package/README.md +46 -5
- package/commands/disable.md +27 -27
- package/commands/enable.md +27 -27
- package/commands/rules.md +32 -0
- package/commands/timeline.md +31 -0
- package/dist/src/cli.js +289 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config/loader.js +5 -2
- package/dist/src/config/loader.js.map +1 -1
- package/dist/src/config/migrate-config.js +33 -1
- package/dist/src/config/migrate-config.js.map +1 -1
- package/dist/src/extractors/index.d.ts +4 -0
- package/dist/src/extractors/index.js +12 -1
- package/dist/src/extractors/index.js.map +1 -1
- package/dist/src/extractors/rules.d.ts +11 -0
- package/dist/src/extractors/rules.js +191 -0
- package/dist/src/extractors/rules.js.map +1 -0
- package/dist/src/extractors/tool-timeline.d.ts +29 -0
- package/dist/src/extractors/tool-timeline.js +732 -0
- package/dist/src/extractors/tool-timeline.js.map +1 -0
- package/dist/src/formatters/index.d.ts +1 -1
- package/dist/src/formatters/index.js +2 -1
- package/dist/src/formatters/index.js.map +1 -1
- package/dist/src/formatters/layout.d.ts +2 -0
- package/dist/src/formatters/layout.js +69 -6
- package/dist/src/formatters/layout.js.map +1 -1
- package/dist/src/i18n/locales/en.js +21 -0
- package/dist/src/i18n/locales/en.js.map +1 -1
- package/dist/src/i18n/locales/zh.js +38 -17
- package/dist/src/i18n/locales/zh.js.map +1 -1
- package/dist/src/index.js +58 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/themes/builtin/cyberpunk.js +2 -0
- package/dist/src/themes/builtin/cyberpunk.js.map +1 -1
- package/dist/src/themes/builtin/dark.js +2 -0
- package/dist/src/themes/builtin/dark.js.map +1 -1
- package/dist/src/themes/builtin/forest.js +2 -0
- package/dist/src/themes/builtin/forest.js.map +1 -1
- package/dist/src/themes/builtin/light.js +2 -0
- package/dist/src/themes/builtin/light.js.map +1 -1
- package/dist/src/themes/builtin/ocean.js +2 -0
- package/dist/src/themes/builtin/ocean.js.map +1 -1
- package/dist/src/themes/icon-sets/nerd.d.ts +2 -0
- package/dist/src/themes/icon-sets/nerd.js +3 -1
- package/dist/src/themes/icon-sets/nerd.js.map +1 -1
- package/dist/src/themes/icon-sets/text.d.ts +2 -0
- package/dist/src/themes/icon-sets/text.js +3 -1
- package/dist/src/themes/icon-sets/text.js.map +1 -1
- package/dist/src/themes/index.js +2 -0
- package/dist/src/themes/index.js.map +1 -1
- package/dist/src/tool-timeline/cache.d.ts +17 -0
- package/dist/src/tool-timeline/cache.js +407 -0
- package/dist/src/tool-timeline/cache.js.map +1 -0
- package/dist/src/types/pulse-config.d.ts +22 -0
- package/dist/src/types/pulse-config.js +28 -2
- package/dist/src/types/pulse-config.js.map +1 -1
- package/dist/src/types/theme.d.ts +2 -0
- package/dist/src/types/tool-timeline.d.ts +101 -0
- package/dist/src/types/tool-timeline.js +3 -0
- package/dist/src/types/tool-timeline.js.map +1 -0
- package/dist/src/utils/cache.js +4 -1
- package/dist/src/utils/cache.js.map +1 -1
- package/dist/src/utils/display-sanitize.js +3 -1
- package/dist/src/utils/display-sanitize.js.map +1 -1
- package/dist/src/utils/terminal-width.d.ts +12 -0
- package/dist/src/utils/terminal-width.js +89 -0
- package/dist/src/utils/terminal-width.js.map +1 -0
- package/dist/test/formatters.test.js +40 -0
- package/dist/test/formatters.test.js.map +1 -1
- package/dist/test/migrate-config.test.js +50 -1
- package/dist/test/migrate-config.test.js.map +1 -1
- package/dist/test/rules.test.d.ts +1 -0
- package/dist/test/rules.test.js +251 -0
- package/dist/test/rules.test.js.map +1 -0
- package/dist/test/terminal-width.test.d.ts +1 -0
- package/dist/test/terminal-width.test.js +98 -0
- package/dist/test/terminal-width.test.js.map +1 -0
- package/dist/test/themes.test.js +6 -1
- package/dist/test/themes.test.js.map +1 -1
- package/dist/test/tool-analytics-index.test.d.ts +1 -0
- package/dist/test/tool-analytics-index.test.js +163 -0
- package/dist/test/tool-analytics-index.test.js.map +1 -0
- package/dist/test/tool-timeline-cache.test.d.ts +1 -0
- package/dist/test/tool-timeline-cache.test.js +239 -0
- package/dist/test/tool-timeline-cache.test.js.map +1 -0
- package/dist/test/tool-timeline-cli.test.d.ts +1 -0
- package/dist/test/tool-timeline-cli.test.js +146 -0
- package/dist/test/tool-timeline-cli.test.js.map +1 -0
- package/dist/test/tool-timeline-normalize.test.d.ts +1 -0
- package/dist/test/tool-timeline-normalize.test.js +206 -0
- package/dist/test/tool-timeline-normalize.test.js.map +1 -0
- package/dist/test/tool-timeline-render.test.d.ts +1 -0
- package/dist/test/tool-timeline-render.test.js +243 -0
- package/dist/test/tool-timeline-render.test.js.map +1 -0
- package/hooks/hooks.json +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.normalizeClaudeToolHook = normalizeClaudeToolHook;
|
|
37
|
+
exports.normalizeClaudeSubagentStopHook = normalizeClaudeSubagentStopHook;
|
|
38
|
+
exports.summarizeTool = summarizeTool;
|
|
39
|
+
exports.summarizeResponse = summarizeResponse;
|
|
40
|
+
exports.summarizeError = summarizeError;
|
|
41
|
+
exports.relativeToCwd = relativeToCwd;
|
|
42
|
+
exports.extractToolTimeline = extractToolTimeline;
|
|
43
|
+
exports.renderToolAnalyticsPanel = renderToolAnalyticsPanel;
|
|
44
|
+
exports.stableHash = stableHash;
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const cache_1 = require("../tool-timeline/cache");
|
|
47
|
+
const i18n_1 = require("../i18n");
|
|
48
|
+
function normalizeClaudeToolHook(input) {
|
|
49
|
+
if (!input || typeof input !== 'object')
|
|
50
|
+
return null;
|
|
51
|
+
const hook = input;
|
|
52
|
+
if (hook.hook_event_name !== 'PostToolUse' &&
|
|
53
|
+
hook.hook_event_name !== 'PostToolUseFailure') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (!isNonEmptyString(hook.session_id) || !isNonEmptyString(hook.tool_name)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const provider = 'claude-code';
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const hookDurationMs = normalizeDuration(hook.duration_ms);
|
|
62
|
+
const agentTelemetry = hook.tool_name === 'Agent'
|
|
63
|
+
? extractAgentTelemetry(hook.tool_input, hook.tool_response)
|
|
64
|
+
: undefined;
|
|
65
|
+
const durationMs = agentTelemetry?.totalDurationMs ?? hookDurationMs;
|
|
66
|
+
const endedAt = now.toISOString();
|
|
67
|
+
const startedAt = durationMs === undefined
|
|
68
|
+
? undefined
|
|
69
|
+
: new Date(now.getTime() - durationMs).toISOString();
|
|
70
|
+
const responseStatus = statusFromResponse(hook.tool_response);
|
|
71
|
+
const status = hook.hook_event_name === 'PostToolUseFailure'
|
|
72
|
+
? 'failure'
|
|
73
|
+
: responseStatus || 'success';
|
|
74
|
+
const tool = summarizeTool(hook.tool_name, hook.tool_input, hook.cwd);
|
|
75
|
+
const responseSummary = summarizeResponse(hook.tool_name, hook.tool_response);
|
|
76
|
+
const errorSummary = summarizeError(hook.error);
|
|
77
|
+
const tokenUsage = extractTokenUsage(hook.tool_response);
|
|
78
|
+
const summary = agentTelemetry?.agentName || tool.summary;
|
|
79
|
+
const subagentMetrics = agentTelemetry && hasAgentMetrics(agentTelemetry)
|
|
80
|
+
? {
|
|
81
|
+
totalToolUseCount: agentTelemetry.totalToolUseCount,
|
|
82
|
+
totalTokens: agentTelemetry.totalTokens,
|
|
83
|
+
totalDurationMs: agentTelemetry.totalDurationMs
|
|
84
|
+
}
|
|
85
|
+
: undefined;
|
|
86
|
+
const toolUseId = isNonEmptyString(hook.tool_use_id) ? hook.tool_use_id : undefined;
|
|
87
|
+
const id = toolUseId
|
|
88
|
+
? `${provider}:${hook.session_id}:${toolUseId}`
|
|
89
|
+
: `${provider}:${hook.session_id}:${stableHash([
|
|
90
|
+
hook.hook_event_name,
|
|
91
|
+
hook.tool_name,
|
|
92
|
+
summary,
|
|
93
|
+
responseSummary || '',
|
|
94
|
+
errorSummary || '',
|
|
95
|
+
endedAt
|
|
96
|
+
].join('|'))}`;
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
provider,
|
|
100
|
+
sessionId: hook.session_id,
|
|
101
|
+
turnId: isNonEmptyString(hook.turn_id) ? hook.turn_id : undefined,
|
|
102
|
+
toolUseId,
|
|
103
|
+
transcriptPath: hook.transcript_path ?? null,
|
|
104
|
+
cwd: hook.cwd,
|
|
105
|
+
toolName: hook.tool_name,
|
|
106
|
+
displayName: tool.displayName,
|
|
107
|
+
summary,
|
|
108
|
+
status,
|
|
109
|
+
startedAt,
|
|
110
|
+
endedAt,
|
|
111
|
+
durationMs,
|
|
112
|
+
actorKind: 'main-agent',
|
|
113
|
+
actorName: agentTelemetry?.agentName,
|
|
114
|
+
agentId: agentTelemetry?.agentId,
|
|
115
|
+
subagentType: agentTelemetry?.subagentType,
|
|
116
|
+
tokenUsage,
|
|
117
|
+
subagentMetrics,
|
|
118
|
+
target: tool.target,
|
|
119
|
+
inputSummary: tool.inputSummary,
|
|
120
|
+
responseSummary,
|
|
121
|
+
errorSummary
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function normalizeClaudeSubagentStopHook(input) {
|
|
125
|
+
if (!input || typeof input !== 'object')
|
|
126
|
+
return null;
|
|
127
|
+
const hook = input;
|
|
128
|
+
if (hook.hook_event_name !== 'SubagentStop')
|
|
129
|
+
return null;
|
|
130
|
+
if (!isNonEmptyString(hook.agent_id))
|
|
131
|
+
return null;
|
|
132
|
+
const agentType = cleanText(hook.agent_type, 80);
|
|
133
|
+
const displayName = agentType || hook.agent_id;
|
|
134
|
+
return {
|
|
135
|
+
agentId: hook.agent_id,
|
|
136
|
+
agentType: agentType || undefined,
|
|
137
|
+
displayName,
|
|
138
|
+
transcriptPath: isNonEmptyString(hook.agent_transcript_path)
|
|
139
|
+
? hook.agent_transcript_path
|
|
140
|
+
: undefined,
|
|
141
|
+
lastSeenAt: new Date().toISOString()
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function summarizeTool(toolName, input, cwd) {
|
|
145
|
+
const obj = isRecord(input) ? input : {};
|
|
146
|
+
const displayName = getDisplayName(toolName);
|
|
147
|
+
if (toolName === 'Agent') {
|
|
148
|
+
const agentName = cleanText(firstStringField(obj, ['subagent_type', 'agent_type', 'description']), 80);
|
|
149
|
+
const summary = agentName || 'Agent';
|
|
150
|
+
return {
|
|
151
|
+
displayName,
|
|
152
|
+
summary,
|
|
153
|
+
target: agentName ? { kind: 'unknown', value: agentName } : undefined,
|
|
154
|
+
inputSummary: summary
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (toolName === 'Bash') {
|
|
158
|
+
const command = cleanText(stringField(obj, 'command'), 160);
|
|
159
|
+
const firstLine = cleanText(firstLineOf(stringField(obj, 'command')), 80);
|
|
160
|
+
const description = cleanText(stringField(obj, 'description'), 80);
|
|
161
|
+
const summary = description && firstLine
|
|
162
|
+
? `${description}: ${firstLine}`
|
|
163
|
+
: firstLine || description || 'Bash';
|
|
164
|
+
return {
|
|
165
|
+
displayName,
|
|
166
|
+
summary: cleanText(summary, 80),
|
|
167
|
+
target: command ? { kind: 'command', value: command } : undefined,
|
|
168
|
+
inputSummary: cleanText(summary, 80)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (toolName === 'Read') {
|
|
172
|
+
const file = summarizeFilePath(stringField(obj, 'file_path'), cwd);
|
|
173
|
+
return fileSummary(displayName, file, file || 'Read');
|
|
174
|
+
}
|
|
175
|
+
if (toolName === 'Write') {
|
|
176
|
+
const file = summarizeFilePath(stringField(obj, 'file_path'), cwd);
|
|
177
|
+
return fileSummary(displayName, file, `write ${file || 'file'}`);
|
|
178
|
+
}
|
|
179
|
+
if (toolName === 'Edit') {
|
|
180
|
+
const file = summarizeFilePath(stringField(obj, 'file_path'), cwd);
|
|
181
|
+
return fileSummary(displayName, file, `edit ${file || 'file'}`);
|
|
182
|
+
}
|
|
183
|
+
if (toolName === 'MultiEdit') {
|
|
184
|
+
const file = summarizeFilePath(stringField(obj, 'file_path'), cwd);
|
|
185
|
+
const edits = Array.isArray(obj.edits) ? obj.edits.length : 0;
|
|
186
|
+
const summary = `multi-edit ${file || 'file'}${edits > 0 ? ` (${edits})` : ''}`;
|
|
187
|
+
return fileSummary(displayName, file, summary);
|
|
188
|
+
}
|
|
189
|
+
if (toolName === 'Glob') {
|
|
190
|
+
const pattern = cleanText(stringField(obj, 'pattern'), 120);
|
|
191
|
+
const summary = pattern ? `glob ${pattern}` : 'glob';
|
|
192
|
+
return {
|
|
193
|
+
displayName,
|
|
194
|
+
summary: cleanText(summary, 80),
|
|
195
|
+
target: pattern ? { kind: 'query', value: pattern } : undefined,
|
|
196
|
+
inputSummary: cleanText(summary, 80)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (toolName === 'Grep') {
|
|
200
|
+
const pattern = cleanText(stringField(obj, 'pattern'), 120);
|
|
201
|
+
const searchPath = cleanText(stringField(obj, 'path'), 80);
|
|
202
|
+
const summary = pattern
|
|
203
|
+
? `grep ${pattern}${searchPath ? ` in ${searchPath}` : ''}`
|
|
204
|
+
: 'grep';
|
|
205
|
+
return {
|
|
206
|
+
displayName,
|
|
207
|
+
summary: cleanText(summary, 80),
|
|
208
|
+
target: pattern ? { kind: 'query', value: pattern } : undefined,
|
|
209
|
+
inputSummary: cleanText(summary, 80)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (toolName === 'WebFetch') {
|
|
213
|
+
const rawUrl = cleanText(stringField(obj, 'url'), 200);
|
|
214
|
+
const label = summarizeUrl(rawUrl);
|
|
215
|
+
const summary = label ? `fetch ${label}` : 'fetch';
|
|
216
|
+
return {
|
|
217
|
+
displayName,
|
|
218
|
+
summary: cleanText(summary, 80),
|
|
219
|
+
target: rawUrl ? { kind: 'url', value: rawUrl } : undefined,
|
|
220
|
+
inputSummary: cleanText(summary, 80)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (toolName === 'WebSearch') {
|
|
224
|
+
const query = cleanText(stringField(obj, 'query'), 120);
|
|
225
|
+
const summary = query ? `search ${query}` : 'search';
|
|
226
|
+
return {
|
|
227
|
+
displayName,
|
|
228
|
+
summary: cleanText(summary, 80),
|
|
229
|
+
target: query ? { kind: 'query', value: query } : undefined,
|
|
230
|
+
inputSummary: cleanText(summary, 80)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (toolName.startsWith('mcp__')) {
|
|
234
|
+
const value = mcpValue(toolName);
|
|
235
|
+
const summary = `mcp ${value}`;
|
|
236
|
+
return {
|
|
237
|
+
displayName,
|
|
238
|
+
summary: cleanText(summary, 80),
|
|
239
|
+
target: { kind: 'mcp', value },
|
|
240
|
+
inputSummary: cleanText(summary, 80)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const fallback = firstKnownField(obj);
|
|
244
|
+
const summary = fallback ? `${toolName} ${fallback}` : toolName;
|
|
245
|
+
return {
|
|
246
|
+
displayName,
|
|
247
|
+
summary: cleanText(summary, 80),
|
|
248
|
+
target: fallback ? { kind: 'unknown', value: fallback } : undefined,
|
|
249
|
+
inputSummary: cleanText(summary, 80)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function summarizeResponse(toolName, response) {
|
|
253
|
+
if (response === undefined || response === null)
|
|
254
|
+
return undefined;
|
|
255
|
+
if (toolName === 'Agent' && isRecord(response)) {
|
|
256
|
+
const totalToolUseCount = numericField(response, 'totalToolUseCount');
|
|
257
|
+
const totalTokens = numericField(response, 'totalTokens');
|
|
258
|
+
const totalDurationMs = numericField(response, 'totalDurationMs');
|
|
259
|
+
const parts = [];
|
|
260
|
+
if (totalToolUseCount !== undefined)
|
|
261
|
+
parts.push(`${totalToolUseCount} tools`);
|
|
262
|
+
if (totalTokens !== undefined)
|
|
263
|
+
parts.push(`${totalTokens} tokens`);
|
|
264
|
+
if (totalDurationMs !== undefined)
|
|
265
|
+
parts.push(formatDurationShort(totalDurationMs));
|
|
266
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
267
|
+
}
|
|
268
|
+
if (toolName === 'Bash' && isRecord(response)) {
|
|
269
|
+
const stderr = stringField(response, 'stderr');
|
|
270
|
+
const stdout = stringField(response, 'stdout');
|
|
271
|
+
if (stderr)
|
|
272
|
+
return `stderr: ${cleanText(firstLineOf(stderr), 120)}`;
|
|
273
|
+
if (stdout)
|
|
274
|
+
return `stdout: ${cleanText(firstLineOf(stdout), 120)}`;
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
if ((toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') && isRecord(response)) {
|
|
278
|
+
const filePath = stringField(response, 'filePath') || stringField(response, 'file_path');
|
|
279
|
+
if (filePath)
|
|
280
|
+
return `file: ${cleanText(filePath, 120)}`;
|
|
281
|
+
if (typeof response.success === 'boolean') {
|
|
282
|
+
return response.success ? 'success' : 'not successful';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (typeof response === 'string') {
|
|
286
|
+
return cleanText(firstLineOf(response), 120);
|
|
287
|
+
}
|
|
288
|
+
if (isRecord(response)) {
|
|
289
|
+
const message = stringField(response, 'message') ||
|
|
290
|
+
stringField(response, 'summary') ||
|
|
291
|
+
stringField(response, 'error');
|
|
292
|
+
if (message)
|
|
293
|
+
return cleanText(firstLineOf(message), 120);
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
function summarizeError(error) {
|
|
298
|
+
if (error === undefined || error === null)
|
|
299
|
+
return undefined;
|
|
300
|
+
if (typeof error === 'string')
|
|
301
|
+
return cleanText(firstLineOf(error), 120);
|
|
302
|
+
if (error instanceof Error)
|
|
303
|
+
return cleanText(firstLineOf(error.message), 120);
|
|
304
|
+
if (isRecord(error)) {
|
|
305
|
+
const message = stringField(error, 'message') || stringField(error, 'error');
|
|
306
|
+
if (message)
|
|
307
|
+
return cleanText(firstLineOf(message), 120);
|
|
308
|
+
}
|
|
309
|
+
return cleanText(String(error), 120);
|
|
310
|
+
}
|
|
311
|
+
function relativeToCwd(filePath, cwd) {
|
|
312
|
+
const cleanPath = cleanText(filePath, 240);
|
|
313
|
+
if (!cleanPath || !cwd)
|
|
314
|
+
return cleanPath;
|
|
315
|
+
try {
|
|
316
|
+
const resolvedCwd = path.resolve(cwd);
|
|
317
|
+
const resolvedFile = path.isAbsolute(cleanPath)
|
|
318
|
+
? path.resolve(cleanPath)
|
|
319
|
+
: path.resolve(resolvedCwd, cleanPath);
|
|
320
|
+
const rel = path.relative(resolvedCwd, resolvedFile);
|
|
321
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
322
|
+
return cleanPath;
|
|
323
|
+
}
|
|
324
|
+
return rel;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return cleanPath;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function extractToolTimeline(sessionId, config, theme, iconOverride) {
|
|
331
|
+
if (!sessionId)
|
|
332
|
+
return null;
|
|
333
|
+
const cache = (0, cache_1.readToolTimelineCache)(sessionId, 'claude-code');
|
|
334
|
+
if (!cache || cache.events.length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
const stats = cache.stats;
|
|
337
|
+
const latest = cache.events[cache.events.length - 1];
|
|
338
|
+
const maxLength = Math.max(20, config.summaryMaxLength || 80);
|
|
339
|
+
const icon = iconOverride ?? theme.components.toolTimeline.icon ?? '';
|
|
340
|
+
const prefix = icon ? `${icon} ` : '';
|
|
341
|
+
const mode = config.mode || 'summary';
|
|
342
|
+
const text = mode === 'compact-list'
|
|
343
|
+
? renderCompactList(cache.events, config, prefix, maxLength)
|
|
344
|
+
: renderSummary(stats, config, prefix, maxLength);
|
|
345
|
+
const slowestMs = stats.slowest?.durationMs || 0;
|
|
346
|
+
const slowThresholdMs = config.slowThresholdMs ?? 3000;
|
|
347
|
+
const fg = (latest.status === 'failure' || stats.failure > 0)
|
|
348
|
+
? theme.colors.error
|
|
349
|
+
: slowestMs >= slowThresholdMs
|
|
350
|
+
? theme.colors.warning
|
|
351
|
+
: theme.colors.info;
|
|
352
|
+
return { text, fg };
|
|
353
|
+
}
|
|
354
|
+
function renderToolAnalyticsPanel(sessionId, config, theme, language, snapshot) {
|
|
355
|
+
if (!sessionId)
|
|
356
|
+
return null;
|
|
357
|
+
const cache = (0, cache_1.readToolTimelineCache)(sessionId, 'claude-code');
|
|
358
|
+
if (!cache || cache.events.length === 0)
|
|
359
|
+
return null;
|
|
360
|
+
const labels = (0, i18n_1.getLabels)(language);
|
|
361
|
+
const stats = cache.analyticsStats ?? (0, cache_1.computeToolAnalyticsStats)(cache.events, cache.agents);
|
|
362
|
+
const width = resolvePanelWidth(config.panelWidth, snapshot?.terminalWidth);
|
|
363
|
+
const border = '\u2550'.repeat(width);
|
|
364
|
+
const divider = '\u2500'.repeat(width);
|
|
365
|
+
const title = centerText(label(labels, 'toolAnalyticsTitle', 'TOOL ANALYTICS'), width);
|
|
366
|
+
const lines = [border, title, border];
|
|
367
|
+
const metricParts = [`${label(labels, 'toolAnalyticsCalls', 'Calls')}: ${stats.totalToolCalls}`];
|
|
368
|
+
const contextTokens = contextTokenCount(snapshot?.contextWindow, snapshot?.cost);
|
|
369
|
+
if (config.showTokenStats !== false && contextTokens !== undefined) {
|
|
370
|
+
metricParts.push(`${label(labels, 'toolAnalyticsContext', 'Context')}: ${formatTokenCount(contextTokens)} ${label(labels, 'toolAnalyticsTokens', 'tok')}`);
|
|
371
|
+
}
|
|
372
|
+
if (config.showSuccessRate !== false) {
|
|
373
|
+
metricParts.push(`${label(labels, 'toolAnalyticsSuccess', 'Success')}: ${stats.successRate}%`);
|
|
374
|
+
}
|
|
375
|
+
pushWrappedPanelItems(lines, metricParts, width);
|
|
376
|
+
if (config.showAgentStats !== false) {
|
|
377
|
+
pushWrappedPanelItems(lines, [
|
|
378
|
+
`${label(labels, 'toolAnalyticsMainAgent', 'Main agent')}: ${stats.mainAgentToolCalls} ${label(labels, 'toolAnalyticsTools', 'tools')}`,
|
|
379
|
+
`${label(labels, 'toolAnalyticsSubagents', 'Subagents')}: ${stats.subagentToolCalls} ${label(labels, 'toolAnalyticsTools', 'tools')} / ${stats.subagentCount} ${label(labels, 'toolAnalyticsAgents', 'agents')}`
|
|
380
|
+
], width);
|
|
381
|
+
const subagentLine = renderSubagentList(stats, labels, width);
|
|
382
|
+
if (subagentLine)
|
|
383
|
+
lines.push(subagentLine);
|
|
384
|
+
}
|
|
385
|
+
if (config.showSlowest !== false && stats.slowest) {
|
|
386
|
+
const slowestSummary = stats.slowest.summary
|
|
387
|
+
? ` "${truncateText(stats.slowest.summary, 24)}"`
|
|
388
|
+
: '';
|
|
389
|
+
lines.push(fitLine(` ${label(labels, 'toolAnalyticsSlowest', 'Slowest')}: ${stats.slowest.toolName}${slowestSummary} ${formatDurationShort(stats.slowest.durationMs)}`, width));
|
|
390
|
+
}
|
|
391
|
+
if (config.showRecent !== false) {
|
|
392
|
+
lines.push(fitLine(` ${label(labels, 'toolAnalyticsRecent', 'Recent')}:`, width));
|
|
393
|
+
const maxRecent = Math.max(1, Math.min(5, Math.floor(config.maxDisplayEvents || 5)));
|
|
394
|
+
const recent = cache.events.slice(Math.max(0, cache.events.length - maxRecent));
|
|
395
|
+
for (const event of recent) {
|
|
396
|
+
lines.push(renderRecentEventLine(event, labels, width));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
lines.push(divider, border);
|
|
400
|
+
void theme;
|
|
401
|
+
return { text: lines.join('\n') };
|
|
402
|
+
}
|
|
403
|
+
function renderSummary(stats, config, prefix, maxLength) {
|
|
404
|
+
const parts = [`${stats.total} calls`];
|
|
405
|
+
if (config.showAverage !== false && stats.avgDurationMs !== undefined) {
|
|
406
|
+
parts.push(`avg ${formatDurationShort(stats.avgDurationMs)}`);
|
|
407
|
+
}
|
|
408
|
+
if (config.showSlowest !== false && stats.slowest) {
|
|
409
|
+
parts.push(`slow ${stats.slowest.toolName} ${formatDurationShort(stats.slowest.durationMs)}`);
|
|
410
|
+
}
|
|
411
|
+
if (config.showFailures !== false && stats.failure > 0) {
|
|
412
|
+
parts.push(`fail ${stats.failure}`);
|
|
413
|
+
}
|
|
414
|
+
return cleanText(`${prefix}${parts.join(' ')}`, maxLength);
|
|
415
|
+
}
|
|
416
|
+
function renderCompactList(events, config, prefix, maxLength) {
|
|
417
|
+
const count = Math.max(1, Math.min(config.maxDisplayEvents || 5, events.length));
|
|
418
|
+
const recent = events.slice(events.length - count);
|
|
419
|
+
const body = recent.map((event) => {
|
|
420
|
+
const marker = event.status === 'failure' ? 'ERR' : 'OK';
|
|
421
|
+
return `${event.displayName} ${marker} ${event.summary}`;
|
|
422
|
+
}).join('; ');
|
|
423
|
+
return cleanText(`${prefix}${body}`, maxLength);
|
|
424
|
+
}
|
|
425
|
+
function renderSubagentList(stats, labels, width) {
|
|
426
|
+
const entries = Object.entries(stats.bySubagent)
|
|
427
|
+
.sort((a, b) => b[1].toolCalls - a[1].toolCalls);
|
|
428
|
+
if (entries.length === 0)
|
|
429
|
+
return null;
|
|
430
|
+
const unknown = label(labels, 'toolAnalyticsUnknownAgent', 'Unknown agent');
|
|
431
|
+
const body = entries
|
|
432
|
+
.map(([name, entry]) => `${name === 'Unknown agent' ? unknown : name} ${entry.toolCalls}`)
|
|
433
|
+
.join(', ');
|
|
434
|
+
return fitLine(` ${label(labels, 'toolAnalyticsSubagents', 'Subagents')}: ${body}`, width);
|
|
435
|
+
}
|
|
436
|
+
function pushWrappedPanelItems(lines, items, width) {
|
|
437
|
+
const indent = ' ';
|
|
438
|
+
const separator = ' \u2502 ';
|
|
439
|
+
const available = Math.max(1, width - indent.length);
|
|
440
|
+
let current = '';
|
|
441
|
+
for (const item of items) {
|
|
442
|
+
const cleanItem = truncateText(item, available);
|
|
443
|
+
const next = current ? `${current}${separator}${cleanItem}` : cleanItem;
|
|
444
|
+
if (current && next.length > available) {
|
|
445
|
+
lines.push(fitLine(`${indent}${current}`, width));
|
|
446
|
+
current = cleanItem;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
current = next;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (current) {
|
|
453
|
+
lines.push(fitLine(`${indent}${current}`, width));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function renderRecentEventLine(event, labels, width) {
|
|
457
|
+
const tool = `[${truncateText(event.displayName || event.toolName, 8)}]`;
|
|
458
|
+
const toolCell = padRight(tool, 9);
|
|
459
|
+
const status = statusMarker(event.status);
|
|
460
|
+
if (event.toolName === 'Agent' && event.subagentMetrics) {
|
|
461
|
+
const metrics = [];
|
|
462
|
+
if (event.subagentMetrics.totalToolUseCount !== undefined) {
|
|
463
|
+
metrics.push(`${event.subagentMetrics.totalToolUseCount} ${label(labels, 'toolAnalyticsTools', 'tools')}`);
|
|
464
|
+
}
|
|
465
|
+
if (event.subagentMetrics.totalTokens !== undefined) {
|
|
466
|
+
metrics.push(`${formatTokenCount(event.subagentMetrics.totalTokens)} ${label(labels, 'toolAnalyticsTokens', 'tok')}`);
|
|
467
|
+
}
|
|
468
|
+
const durationMs = event.subagentMetrics.totalDurationMs ?? event.durationMs;
|
|
469
|
+
if (durationMs !== undefined)
|
|
470
|
+
metrics.push(formatDurationShort(durationMs));
|
|
471
|
+
const tail = ` ${metrics.join(' ')} ${status}`.trimEnd();
|
|
472
|
+
const left = ` ${toolCell} `;
|
|
473
|
+
const summaryWidth = Math.max(8, width - left.length - tail.length - 1);
|
|
474
|
+
return fitLine(`${left}${padRight(truncateText(event.actorName || event.summary, summaryWidth), summaryWidth)} ${tail}`, width);
|
|
475
|
+
}
|
|
476
|
+
const duration = event.durationMs === undefined ? '-' : formatDurationShort(event.durationMs);
|
|
477
|
+
const tail = `${padLeft(duration, 7)} ${status}`;
|
|
478
|
+
const left = ` ${toolCell} `;
|
|
479
|
+
const summaryWidth = Math.max(8, width - left.length - tail.length);
|
|
480
|
+
return fitLine(`${left}${padRight(truncateText(event.summary, summaryWidth), summaryWidth)}${tail}`, width);
|
|
481
|
+
}
|
|
482
|
+
function contextTokenCount(contextWindow, cost) {
|
|
483
|
+
const currentUsage = contextWindow?.current_usage ?? cost?.current_usage;
|
|
484
|
+
if (currentUsage) {
|
|
485
|
+
return sumNumbers([
|
|
486
|
+
currentUsage.input_tokens,
|
|
487
|
+
currentUsage.output_tokens,
|
|
488
|
+
currentUsage.cache_creation_input_tokens,
|
|
489
|
+
currentUsage.cache_read_input_tokens
|
|
490
|
+
]);
|
|
491
|
+
}
|
|
492
|
+
if (contextWindow) {
|
|
493
|
+
return sumNumbers([
|
|
494
|
+
contextWindow.total_input_tokens,
|
|
495
|
+
contextWindow.total_output_tokens
|
|
496
|
+
]);
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
function sumNumbers(values) {
|
|
501
|
+
return values.reduce((sum, value) => (typeof value === 'number' && Number.isFinite(value) ? sum + value : sum), 0);
|
|
502
|
+
}
|
|
503
|
+
function statusMarker(status) {
|
|
504
|
+
if (status === 'success')
|
|
505
|
+
return '\u2713';
|
|
506
|
+
if (status === 'failure')
|
|
507
|
+
return '\u2717';
|
|
508
|
+
return '?';
|
|
509
|
+
}
|
|
510
|
+
function formatTokenCount(value) {
|
|
511
|
+
if (value >= 1_000_000)
|
|
512
|
+
return `${trimFixed(value / 1_000_000)}M`;
|
|
513
|
+
if (value >= 1000)
|
|
514
|
+
return `${trimFixed(value / 1000)}K`;
|
|
515
|
+
return `${Math.round(value)}`;
|
|
516
|
+
}
|
|
517
|
+
function trimFixed(value) {
|
|
518
|
+
return value >= 10 ? value.toFixed(0) : value.toFixed(1);
|
|
519
|
+
}
|
|
520
|
+
function label(labels, key, fallback) {
|
|
521
|
+
return labels[key] || fallback;
|
|
522
|
+
}
|
|
523
|
+
function centerText(value, width) {
|
|
524
|
+
const clean = truncateText(value, width);
|
|
525
|
+
const left = Math.max(0, Math.floor((width - clean.length) / 2));
|
|
526
|
+
return `${' '.repeat(left)}${clean}`;
|
|
527
|
+
}
|
|
528
|
+
function fitLine(value, width) {
|
|
529
|
+
return truncateText(value, width);
|
|
530
|
+
}
|
|
531
|
+
function truncateText(value, maxLength) {
|
|
532
|
+
if (!value)
|
|
533
|
+
return '';
|
|
534
|
+
const withoutAnsi = value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
535
|
+
const withoutControls = withoutAnsi
|
|
536
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
537
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
538
|
+
if (withoutControls.length <= maxLength)
|
|
539
|
+
return withoutControls;
|
|
540
|
+
return `${withoutControls.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
541
|
+
}
|
|
542
|
+
function padRight(value, width) {
|
|
543
|
+
return value.length >= width ? value : `${value}${' '.repeat(width - value.length)}`;
|
|
544
|
+
}
|
|
545
|
+
function padLeft(value, width) {
|
|
546
|
+
return value.length >= width ? value : `${' '.repeat(width - value.length)}${value}`;
|
|
547
|
+
}
|
|
548
|
+
function clampNumber(value, min, max) {
|
|
549
|
+
if (!Number.isFinite(value))
|
|
550
|
+
return min;
|
|
551
|
+
return Math.min(max, Math.max(min, value));
|
|
552
|
+
}
|
|
553
|
+
function resolvePanelWidth(configWidth, terminalWidth) {
|
|
554
|
+
const preferred = Math.floor(configWidth || 59);
|
|
555
|
+
const terminal = typeof terminalWidth === 'number' && Number.isFinite(terminalWidth)
|
|
556
|
+
? Math.floor(terminalWidth)
|
|
557
|
+
: undefined;
|
|
558
|
+
const maxWidth = terminal && terminal > 0 ? Math.min(120, terminal) : 120;
|
|
559
|
+
return clampNumber(preferred, 30, maxWidth);
|
|
560
|
+
}
|
|
561
|
+
function stableHash(value) {
|
|
562
|
+
let hash = 5381;
|
|
563
|
+
for (let i = 0; i < value.length; i++) {
|
|
564
|
+
hash = ((hash << 5) + hash) ^ value.charCodeAt(i);
|
|
565
|
+
}
|
|
566
|
+
return (hash >>> 0).toString(36);
|
|
567
|
+
}
|
|
568
|
+
function getDisplayName(toolName) {
|
|
569
|
+
return toolName.startsWith('mcp__') ? 'MCP' : toolName;
|
|
570
|
+
}
|
|
571
|
+
function fileSummary(displayName, file, summary) {
|
|
572
|
+
return {
|
|
573
|
+
displayName,
|
|
574
|
+
summary: cleanText(summary, 80),
|
|
575
|
+
target: file ? { kind: 'file', value: file } : undefined,
|
|
576
|
+
inputSummary: cleanText(summary, 80)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function summarizeFilePath(filePath, cwd) {
|
|
580
|
+
const rel = relativeToCwd(filePath, cwd);
|
|
581
|
+
return cleanText(rel || path.basename(filePath || ''), 160);
|
|
582
|
+
}
|
|
583
|
+
function mcpValue(toolName) {
|
|
584
|
+
const parts = toolName.split('__').filter(Boolean);
|
|
585
|
+
if (parts.length >= 3 && parts[0] === 'mcp') {
|
|
586
|
+
return `${parts[1]}.${parts.slice(2).join('.')}`;
|
|
587
|
+
}
|
|
588
|
+
return toolName.replace(/^mcp__/, '').replace(/__/g, '.');
|
|
589
|
+
}
|
|
590
|
+
function summarizeUrl(rawUrl) {
|
|
591
|
+
if (!rawUrl)
|
|
592
|
+
return '';
|
|
593
|
+
try {
|
|
594
|
+
const url = new URL(rawUrl);
|
|
595
|
+
return cleanText(`${url.host}${url.pathname}`, 120);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return cleanText(rawUrl, 120);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function extractAgentTelemetry(input, response) {
|
|
602
|
+
const inputObj = isRecord(input) ? input : {};
|
|
603
|
+
const responseObj = isRecord(response) ? response : {};
|
|
604
|
+
const agentId = firstStringField(responseObj, ['agentId', 'agent_id', 'id']);
|
|
605
|
+
const subagentType = firstStringField(inputObj, ['subagent_type', 'agent_type']);
|
|
606
|
+
const agentName = cleanText(subagentType ||
|
|
607
|
+
stringField(inputObj, 'description') ||
|
|
608
|
+
firstStringField(responseObj, ['agentType', 'agent_type', 'name']) ||
|
|
609
|
+
agentId ||
|
|
610
|
+
'Unknown agent', 80);
|
|
611
|
+
return {
|
|
612
|
+
agentId: agentId || undefined,
|
|
613
|
+
agentName,
|
|
614
|
+
subagentType: subagentType || undefined,
|
|
615
|
+
totalToolUseCount: numericField(responseObj, 'totalToolUseCount') ??
|
|
616
|
+
numericField(responseObj, 'total_tool_use_count') ??
|
|
617
|
+
numericField(responseObj, 'toolUseCount') ??
|
|
618
|
+
numericField(responseObj, 'tool_use_count'),
|
|
619
|
+
totalTokens: numericField(responseObj, 'totalTokens') ??
|
|
620
|
+
numericField(responseObj, 'total_tokens'),
|
|
621
|
+
totalDurationMs: numericField(responseObj, 'totalDurationMs') ??
|
|
622
|
+
numericField(responseObj, 'total_duration_ms')
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function extractTokenUsage(response) {
|
|
626
|
+
if (!isRecord(response))
|
|
627
|
+
return undefined;
|
|
628
|
+
const usage = isRecord(response.usage) ? response.usage : response;
|
|
629
|
+
const tokenUsage = {
|
|
630
|
+
inputTokens: numericField(usage, 'input_tokens') ?? numericField(usage, 'inputTokens'),
|
|
631
|
+
outputTokens: numericField(usage, 'output_tokens') ?? numericField(usage, 'outputTokens'),
|
|
632
|
+
cacheCreationInputTokens: numericField(usage, 'cache_creation_input_tokens') ??
|
|
633
|
+
numericField(usage, 'cacheCreationInputTokens'),
|
|
634
|
+
cacheReadInputTokens: numericField(usage, 'cache_read_input_tokens') ??
|
|
635
|
+
numericField(usage, 'cacheReadInputTokens'),
|
|
636
|
+
totalTokens: numericField(usage, 'total_tokens') ?? numericField(usage, 'totalTokens')
|
|
637
|
+
};
|
|
638
|
+
return Object.values(tokenUsage).some((value) => value !== undefined)
|
|
639
|
+
? tokenUsage
|
|
640
|
+
: undefined;
|
|
641
|
+
}
|
|
642
|
+
function hasAgentMetrics(agent) {
|
|
643
|
+
return agent.totalToolUseCount !== undefined ||
|
|
644
|
+
agent.totalTokens !== undefined ||
|
|
645
|
+
agent.totalDurationMs !== undefined;
|
|
646
|
+
}
|
|
647
|
+
function statusFromResponse(response) {
|
|
648
|
+
if (!isRecord(response))
|
|
649
|
+
return undefined;
|
|
650
|
+
const status = firstStringField(response, ['status', 'state']).toLowerCase();
|
|
651
|
+
if (!status)
|
|
652
|
+
return undefined;
|
|
653
|
+
if (status === 'success' || status === 'ok' || status === 'completed')
|
|
654
|
+
return 'success';
|
|
655
|
+
if (status === 'failure' || status === 'failed' || status === 'error')
|
|
656
|
+
return 'failure';
|
|
657
|
+
return 'unknown';
|
|
658
|
+
}
|
|
659
|
+
function normalizeDuration(durationMs) {
|
|
660
|
+
return typeof durationMs === 'number' &&
|
|
661
|
+
Number.isFinite(durationMs) &&
|
|
662
|
+
durationMs >= 0
|
|
663
|
+
? durationMs
|
|
664
|
+
: undefined;
|
|
665
|
+
}
|
|
666
|
+
function firstKnownField(obj) {
|
|
667
|
+
const keys = ['command', 'file_path', 'path', 'pattern', 'query', 'url', 'description'];
|
|
668
|
+
for (const key of keys) {
|
|
669
|
+
const value = stringField(obj, key);
|
|
670
|
+
if (value)
|
|
671
|
+
return cleanText(value, 80);
|
|
672
|
+
}
|
|
673
|
+
return '';
|
|
674
|
+
}
|
|
675
|
+
function cleanText(value, maxLength) {
|
|
676
|
+
if (!value)
|
|
677
|
+
return '';
|
|
678
|
+
const withoutAnsi = value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
679
|
+
const withoutControls = withoutAnsi
|
|
680
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
681
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
682
|
+
.replace(/\s+/g, ' ')
|
|
683
|
+
.trim();
|
|
684
|
+
if (withoutControls.length <= maxLength)
|
|
685
|
+
return withoutControls;
|
|
686
|
+
return `${withoutControls.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
687
|
+
}
|
|
688
|
+
function firstLineOf(value) {
|
|
689
|
+
if (!value)
|
|
690
|
+
return '';
|
|
691
|
+
return value.split(/\r?\n/, 1)[0];
|
|
692
|
+
}
|
|
693
|
+
function stringField(obj, key) {
|
|
694
|
+
const value = obj[key];
|
|
695
|
+
return typeof value === 'string' ? value : '';
|
|
696
|
+
}
|
|
697
|
+
function firstStringField(obj, keys) {
|
|
698
|
+
for (const key of keys) {
|
|
699
|
+
const value = stringField(obj, key);
|
|
700
|
+
if (value)
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
return '';
|
|
704
|
+
}
|
|
705
|
+
function numericField(obj, key) {
|
|
706
|
+
const value = obj[key];
|
|
707
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
708
|
+
return value;
|
|
709
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
710
|
+
const parsed = Number(value);
|
|
711
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
712
|
+
}
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
function isRecord(value) {
|
|
716
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
717
|
+
}
|
|
718
|
+
function isNonEmptyString(value) {
|
|
719
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
720
|
+
}
|
|
721
|
+
function formatDurationShort(ms) {
|
|
722
|
+
if (ms < 1000)
|
|
723
|
+
return `${Math.round(ms)}ms`;
|
|
724
|
+
const seconds = ms / 1000;
|
|
725
|
+
if (seconds < 60) {
|
|
726
|
+
return seconds >= 10 ? `${Math.round(seconds)}s` : `${seconds.toFixed(1)}s`;
|
|
727
|
+
}
|
|
728
|
+
const minutes = Math.floor(seconds / 60);
|
|
729
|
+
const rest = Math.round(seconds % 60);
|
|
730
|
+
return `${minutes}m ${rest}s`;
|
|
731
|
+
}
|
|
732
|
+
//# sourceMappingURL=tool-timeline.js.map
|