sidekick-shared 0.27.5 → 0.28.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/CHANGELOG.md +58 -0
- package/README.md +93 -52
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +15 -8
- package/dist/claudeProfiles.js +30 -23
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +30 -2
- package/dist/codexPaths.js +118 -7
- package/dist/codexProfiles.d.ts +38 -35
- package/dist/codexProfiles.js +848 -715
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/doctor.js +2 -2
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +72 -15
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session dump formatters — text, markdown, and JSON output.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from sidekick-cli's dump command so both the CLI and VS Code
|
|
5
|
+
* extension can produce identical session reports from AggregatedMetrics.
|
|
6
|
+
*
|
|
7
|
+
* @module formatters/sessionDump
|
|
8
|
+
*/
|
|
9
|
+
import { describeCostProvenance } from '../aggregation/costProvenance';
|
|
10
|
+
import { formatDurationMs, formatTokenCount } from '../formatting';
|
|
11
|
+
import { formatTokenBreakdown, TOKEN_MAIN_THREAD_LABEL, TOKEN_SESSION_TOTAL_LABEL, TOKEN_SUBAGENTS_LABEL, TOKEN_TOTAL_LABEL, } from '../tokenSummary';
|
|
12
|
+
import { combineSessionTokenTotals } from '../sessionTokenTotals';
|
|
13
|
+
/** Main thread (the aggregated transcript) plus the session's subagents. */
|
|
14
|
+
function sessionTokens(metrics, options) {
|
|
15
|
+
return combineSessionTokenTotals(metrics.tokens, options.subagents ?? []);
|
|
16
|
+
}
|
|
17
|
+
// ── JSON output ──
|
|
18
|
+
/**
|
|
19
|
+
* Serialize aggregated metrics as pretty-printed JSON, plus a `tokenSummary`
|
|
20
|
+
* block in the shared vocabulary: `mainThread`, per-subagent summaries,
|
|
21
|
+
* `subagentTotal`, and `combined` (main thread plus subagents).
|
|
22
|
+
*/
|
|
23
|
+
export function formatSessionJson(metrics, options = {}) {
|
|
24
|
+
return (JSON.stringify({ ...metrics, tokenSummary: sessionTokens(metrics, options) }, null, 2) + '\n');
|
|
25
|
+
}
|
|
26
|
+
// ── Text output ──
|
|
27
|
+
/**
|
|
28
|
+
* Format aggregated metrics as a plain-text report.
|
|
29
|
+
*/
|
|
30
|
+
export function formatSessionText(metrics, options = {}) {
|
|
31
|
+
const width = options.width ?? 120;
|
|
32
|
+
const expand = options.expand ?? false;
|
|
33
|
+
const lines = [];
|
|
34
|
+
const hr = '\u2500'.repeat(Math.min(width, 80));
|
|
35
|
+
// Header
|
|
36
|
+
lines.push(hr);
|
|
37
|
+
lines.push(formatHeader(metrics));
|
|
38
|
+
lines.push(hr);
|
|
39
|
+
lines.push('');
|
|
40
|
+
// Token summary
|
|
41
|
+
lines.push(...formatTokenSummary(metrics, sessionTokens(metrics, options)));
|
|
42
|
+
lines.push('');
|
|
43
|
+
// Model stats
|
|
44
|
+
if (metrics.modelStats.length > 0) {
|
|
45
|
+
lines.push('Models:');
|
|
46
|
+
for (const m of metrics.modelStats) {
|
|
47
|
+
lines.push(` ${m.model}: ${m.calls} calls, ${fmtTokens(m.tokens)} total incl. cache, ${fmtCost(m.cost)}`);
|
|
48
|
+
}
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
// Tool stats
|
|
52
|
+
if (metrics.toolStats.length > 0) {
|
|
53
|
+
lines.push('Tools:');
|
|
54
|
+
const sorted = [...metrics.toolStats].sort((a, b) => b.successCount + b.failureCount - (a.successCount + a.failureCount));
|
|
55
|
+
for (const t of sorted) {
|
|
56
|
+
const total = t.successCount + t.failureCount;
|
|
57
|
+
const failStr = t.failureCount > 0 ? ` (${t.failureCount} failed)` : '';
|
|
58
|
+
const avgMs = t.completedCount > 0 ? Math.round(t.totalDuration / t.completedCount) : 0;
|
|
59
|
+
const durationStr = avgMs > 0 ? ` avg ${avgMs}ms` : '';
|
|
60
|
+
lines.push(` ${t.name}: ${total}${failStr}${durationStr}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push('');
|
|
63
|
+
}
|
|
64
|
+
// Compaction / truncation
|
|
65
|
+
if (metrics.compactionCount > 0 || metrics.truncationCount > 0) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (metrics.compactionCount > 0)
|
|
68
|
+
parts.push(`${metrics.compactionCount} compaction(s)`);
|
|
69
|
+
if (metrics.truncationCount > 0)
|
|
70
|
+
parts.push(`${metrics.truncationCount} truncation(s)`);
|
|
71
|
+
lines.push(`Context management: ${parts.join(', ')}`);
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
// Timeline
|
|
75
|
+
lines.push(hr);
|
|
76
|
+
lines.push('Timeline:');
|
|
77
|
+
lines.push(hr);
|
|
78
|
+
const filteredEvents = expand
|
|
79
|
+
? metrics.timeline
|
|
80
|
+
: metrics.timeline.filter((e) => e.noiseLevel !== 'noise');
|
|
81
|
+
if (filteredEvents.length === 0) {
|
|
82
|
+
lines.push(' (no events)');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const event of filteredEvents) {
|
|
86
|
+
const ts = formatTimestamp(event.timestamp);
|
|
87
|
+
const icon = getTimelineEventIcon(event.type);
|
|
88
|
+
const summary = truncateToWidth(event.description, width - ts.length - 4);
|
|
89
|
+
lines.push(`${ts} ${icon} ${summary}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
// ── Markdown output ──
|
|
96
|
+
/**
|
|
97
|
+
* Format aggregated metrics as a markdown report.
|
|
98
|
+
*/
|
|
99
|
+
export function formatSessionMarkdown(metrics, options = {}) {
|
|
100
|
+
const expand = options.expand ?? false;
|
|
101
|
+
const sessionFileName = options.sessionFileName;
|
|
102
|
+
const lines = [];
|
|
103
|
+
// Title
|
|
104
|
+
lines.push('# Session Report');
|
|
105
|
+
lines.push('');
|
|
106
|
+
// Summary
|
|
107
|
+
lines.push('## Summary');
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(`| Metric | Value |`);
|
|
110
|
+
lines.push(`|--------|-------|`);
|
|
111
|
+
if (sessionFileName) {
|
|
112
|
+
lines.push(`| Session file | \`${sessionFileName}\` |`);
|
|
113
|
+
}
|
|
114
|
+
lines.push(`| Started | ${metrics.sessionStartTime || 'N/A'} |`);
|
|
115
|
+
lines.push(`| Last event | ${metrics.lastEventTime || 'N/A'} |`);
|
|
116
|
+
lines.push(`| Duration | ${formatDuration(metrics.sessionStartTime, metrics.lastEventTime)} |`);
|
|
117
|
+
lines.push(`| Messages | ${metrics.messageCount} |`);
|
|
118
|
+
lines.push(`| Events | ${metrics.eventCount} |`);
|
|
119
|
+
lines.push(`| Model | ${metrics.currentModel || 'N/A'} |`);
|
|
120
|
+
lines.push(`| Provider | ${metrics.providerId || 'N/A'} |`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
// Tokens
|
|
123
|
+
lines.push('## Tokens');
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`| Category | Count |`);
|
|
126
|
+
lines.push(`|----------|-------|`);
|
|
127
|
+
lines.push(`| Input | ${fmtTokens(metrics.tokens.inputTokens)} |`);
|
|
128
|
+
lines.push(`| Output | ${fmtTokens(metrics.tokens.outputTokens)} |`);
|
|
129
|
+
lines.push(`| Cache write | ${fmtTokens(metrics.tokens.cacheWriteTokens)} |`);
|
|
130
|
+
lines.push(`| Cache read | ${fmtTokens(metrics.tokens.cacheReadTokens)} |`);
|
|
131
|
+
const tokens = sessionTokens(metrics, options);
|
|
132
|
+
if (tokens.mainThread.billedOutsideBuckets > 0) {
|
|
133
|
+
lines.push(`| Reasoning (billed separately) | ${fmtTokens(tokens.mainThread.billedOutsideBuckets)} |`);
|
|
134
|
+
}
|
|
135
|
+
if (tokens.subagents.length > 0) {
|
|
136
|
+
lines.push(`| ${TOKEN_MAIN_THREAD_LABEL} — ${TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.mainThread.total)} |`);
|
|
137
|
+
lines.push(`| ${TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length}) — ${TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.subagentTotal.total)} |`);
|
|
138
|
+
lines.push(`| **${TOKEN_SESSION_TOTAL_LABEL}** | **${fmtTokens(tokens.combined.total)}** |`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
lines.push(`| ${TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.mainThread.total)} |`);
|
|
142
|
+
}
|
|
143
|
+
if (metrics.tokens.costUsd > 0) {
|
|
144
|
+
lines.push(`| Cost | ${fmtCost(metrics.tokens.costUsd)} (${describeCostProvenance(metrics.tokens)}) |`);
|
|
145
|
+
}
|
|
146
|
+
else if (metrics.tokens.unpricedCalls > 0) {
|
|
147
|
+
lines.push(`| Cost | — (${describeCostProvenance(metrics.tokens)}) |`);
|
|
148
|
+
}
|
|
149
|
+
lines.push('');
|
|
150
|
+
// Model stats
|
|
151
|
+
if (metrics.modelStats.length > 0) {
|
|
152
|
+
lines.push('## Models');
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(`| Model | Calls | ${TOKEN_TOTAL_LABEL} | Cost |`);
|
|
155
|
+
lines.push(`|-------|-------|--------|------|`);
|
|
156
|
+
for (const m of metrics.modelStats) {
|
|
157
|
+
lines.push(`| ${m.model} | ${m.calls} | ${fmtTokens(m.tokens)} | ${fmtCost(m.cost)} |`);
|
|
158
|
+
}
|
|
159
|
+
lines.push('');
|
|
160
|
+
}
|
|
161
|
+
// Tool calls
|
|
162
|
+
if (metrics.toolStats.length > 0) {
|
|
163
|
+
lines.push('## Tool Calls');
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push(`| Tool | Total | Failed | Avg Duration |`);
|
|
166
|
+
lines.push(`|------|-------|--------|--------------|`);
|
|
167
|
+
const sorted = [...metrics.toolStats].sort((a, b) => b.successCount + b.failureCount - (a.successCount + a.failureCount));
|
|
168
|
+
for (const t of sorted) {
|
|
169
|
+
const total = t.successCount + t.failureCount;
|
|
170
|
+
const avgMs = t.completedCount > 0 ? Math.round(t.totalDuration / t.completedCount) : 0;
|
|
171
|
+
lines.push(`| ${t.name} | ${total} | ${t.failureCount} | ${avgMs > 0 ? `${avgMs}ms` : '-'} |`);
|
|
172
|
+
}
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
// Context events
|
|
176
|
+
if (metrics.compactionCount > 0 || metrics.truncationCount > 0) {
|
|
177
|
+
lines.push('## Context Management');
|
|
178
|
+
lines.push('');
|
|
179
|
+
if (metrics.compactionCount > 0) {
|
|
180
|
+
lines.push(`- **Compactions:** ${metrics.compactionCount}`);
|
|
181
|
+
for (const c of metrics.compactionEvents) {
|
|
182
|
+
lines.push(` - At ${formatTimestamp(c.timestamp instanceof Date ? c.timestamp.toISOString() : String(c.timestamp))}: ${fmtTokens(c.contextBefore)} -> ${fmtTokens(c.contextAfter)} tokens`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (metrics.truncationCount > 0) {
|
|
186
|
+
lines.push(`- **Truncations:** ${metrics.truncationCount}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
// Subagents
|
|
191
|
+
if (metrics.subagents.length > 0) {
|
|
192
|
+
lines.push('## Subagents');
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push(`| Type | Description | Status | Duration |`);
|
|
195
|
+
lines.push(`|------|-------------|--------|----------|`);
|
|
196
|
+
for (const s of metrics.subagents) {
|
|
197
|
+
const dur = s.durationMs ? `${Math.round(s.durationMs / 1000)}s` : '-';
|
|
198
|
+
lines.push(`| ${s.subagentType} | ${s.description} | ${s.status} | ${dur} |`);
|
|
199
|
+
}
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
if (tokens.subagents.length > 0) {
|
|
203
|
+
lines.push('## Subagent Tokens');
|
|
204
|
+
lines.push('');
|
|
205
|
+
lines.push(`| Agent | Type | Description | ${TOKEN_TOTAL_LABEL} |`);
|
|
206
|
+
lines.push(`|-------|------|-------------|--------|`);
|
|
207
|
+
for (const agent of tokens.subagents) {
|
|
208
|
+
lines.push(`| ${agent.agentId} | ${agent.agentType ?? '-'} | ${agent.description ?? '-'} | ${fmtTokens(agent.summary.total)} |`);
|
|
209
|
+
}
|
|
210
|
+
lines.push('');
|
|
211
|
+
}
|
|
212
|
+
// Timeline
|
|
213
|
+
lines.push('## Timeline');
|
|
214
|
+
lines.push('');
|
|
215
|
+
const filteredEvents = expand
|
|
216
|
+
? metrics.timeline
|
|
217
|
+
: metrics.timeline.filter((e) => e.noiseLevel !== 'noise');
|
|
218
|
+
if (filteredEvents.length === 0) {
|
|
219
|
+
lines.push('_(no events)_');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
lines.push('```');
|
|
223
|
+
for (const event of filteredEvents) {
|
|
224
|
+
const ts = formatTimestamp(event.timestamp);
|
|
225
|
+
const icon = getTimelineEventIcon(event.type);
|
|
226
|
+
lines.push(`${ts} ${icon} ${event.description}`);
|
|
227
|
+
}
|
|
228
|
+
lines.push('```');
|
|
229
|
+
}
|
|
230
|
+
lines.push('');
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
// ── Formatting helpers ──
|
|
234
|
+
function formatHeader(metrics) {
|
|
235
|
+
const parts = [];
|
|
236
|
+
if (metrics.providerId)
|
|
237
|
+
parts.push(metrics.providerId);
|
|
238
|
+
if (metrics.currentModel)
|
|
239
|
+
parts.push(metrics.currentModel);
|
|
240
|
+
const duration = formatDuration(metrics.sessionStartTime, metrics.lastEventTime);
|
|
241
|
+
if (duration !== 'N/A')
|
|
242
|
+
parts.push(duration);
|
|
243
|
+
parts.push(`${metrics.messageCount} messages`);
|
|
244
|
+
parts.push(`${metrics.eventCount} events`);
|
|
245
|
+
return parts.join(' | ');
|
|
246
|
+
}
|
|
247
|
+
function formatTokenSummary(metrics, tokens) {
|
|
248
|
+
const t = metrics.tokens;
|
|
249
|
+
let first = `Tokens: ${formatTokenBreakdown(tokens.mainThread, fmtTokens)}`;
|
|
250
|
+
if (t.costUsd > 0)
|
|
251
|
+
first += ` Cost: ${fmtCost(t.costUsd)} (${describeCostProvenance(t)})`;
|
|
252
|
+
const lines = [first];
|
|
253
|
+
if (tokens.subagents.length > 0) {
|
|
254
|
+
lines.push(`${TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length}): ${formatTokenBreakdown(tokens.subagentTotal, fmtTokens)}`);
|
|
255
|
+
lines.push(`${TOKEN_SESSION_TOTAL_LABEL}: ${fmtTokens(tokens.combined.total)}`);
|
|
256
|
+
}
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
259
|
+
/** @internal Exported for tests. */
|
|
260
|
+
export function fmtTokens(n) {
|
|
261
|
+
return formatTokenCount(n);
|
|
262
|
+
}
|
|
263
|
+
/** @internal Exported for tests. */
|
|
264
|
+
export function fmtCost(cost) {
|
|
265
|
+
if (cost <= 0)
|
|
266
|
+
return '$0.00';
|
|
267
|
+
if (cost < 0.01)
|
|
268
|
+
return `$${cost.toFixed(4)}`;
|
|
269
|
+
return `$${cost.toFixed(2)}`;
|
|
270
|
+
}
|
|
271
|
+
/** @internal Exported for tests. */
|
|
272
|
+
export function formatTimestamp(iso) {
|
|
273
|
+
try {
|
|
274
|
+
const d = new Date(iso);
|
|
275
|
+
if (isNaN(d.getTime()))
|
|
276
|
+
return '??:??:??';
|
|
277
|
+
const h = d.getHours().toString().padStart(2, '0');
|
|
278
|
+
const m = d.getMinutes().toString().padStart(2, '0');
|
|
279
|
+
const s = d.getSeconds().toString().padStart(2, '0');
|
|
280
|
+
return `${h}:${m}:${s}`;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return '??:??:??';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** @internal Exported for tests. */
|
|
287
|
+
export function formatDuration(start, end) {
|
|
288
|
+
if (!start || !end)
|
|
289
|
+
return 'N/A';
|
|
290
|
+
try {
|
|
291
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
292
|
+
return formatDurationMs(ms);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return 'N/A';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Map TimelineEvent.type to a display icon. */
|
|
299
|
+
function getTimelineEventIcon(type) {
|
|
300
|
+
switch (type) {
|
|
301
|
+
case 'user_prompt':
|
|
302
|
+
return '\u25b6'; // right-pointing triangle
|
|
303
|
+
case 'assistant_response':
|
|
304
|
+
return '\u2726'; // four-pointed star
|
|
305
|
+
case 'tool_call':
|
|
306
|
+
return '\u2699'; // gear
|
|
307
|
+
case 'tool_result':
|
|
308
|
+
return '\u2190'; // left arrow
|
|
309
|
+
case 'compaction':
|
|
310
|
+
return '\u21bb'; // clockwise arrow
|
|
311
|
+
case 'error':
|
|
312
|
+
return '\u2718'; // heavy ballot X
|
|
313
|
+
case 'session_start':
|
|
314
|
+
return '\u25cf'; // black circle
|
|
315
|
+
case 'session_end':
|
|
316
|
+
return '\u25cb'; // white circle
|
|
317
|
+
default:
|
|
318
|
+
return ' ';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function truncateToWidth(text, maxLen) {
|
|
322
|
+
if (maxLen < 10)
|
|
323
|
+
maxLen = 10;
|
|
324
|
+
const clean = text.replace(/\s+/g, ' ').trim();
|
|
325
|
+
if (clean.length <= maxLen)
|
|
326
|
+
return clean;
|
|
327
|
+
return clean.substring(0, maxLen - 3) + '...';
|
|
328
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich tool summary formatters.
|
|
3
|
+
*
|
|
4
|
+
* Registry of per-tool formatters that produce contextual one-liner summaries.
|
|
5
|
+
* Replaces the generic field-fallback `summarizeToolInput()` with tool-specific
|
|
6
|
+
* formatting inspired by tail-claude's 19+ tool formatters.
|
|
7
|
+
*
|
|
8
|
+
* @module formatters/toolSummary
|
|
9
|
+
*/
|
|
10
|
+
// ── Helpers ──
|
|
11
|
+
function truncate(text, maxLen) {
|
|
12
|
+
const clean = text.replace(/\s+/g, ' ').trim();
|
|
13
|
+
return clean.length > maxLen ? clean.substring(0, maxLen - 3) + '...' : clean;
|
|
14
|
+
}
|
|
15
|
+
function basename(filePath) {
|
|
16
|
+
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
17
|
+
return parts[parts.length - 1] || filePath;
|
|
18
|
+
}
|
|
19
|
+
function hostname(url) {
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(url);
|
|
22
|
+
return u.hostname + (u.pathname !== '/' ? u.pathname : '');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return truncate(url, 60);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function countLines(content) {
|
|
29
|
+
if (typeof content !== 'string')
|
|
30
|
+
return null;
|
|
31
|
+
const lines = content.split('\n').length;
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
// ── Per-Tool Formatters ──
|
|
35
|
+
const formatRead = (input) => {
|
|
36
|
+
const filePath = input.file_path;
|
|
37
|
+
if (!filePath)
|
|
38
|
+
return '';
|
|
39
|
+
const name = basename(filePath);
|
|
40
|
+
const offset = input.offset;
|
|
41
|
+
const limit = input.limit;
|
|
42
|
+
if (offset && limit)
|
|
43
|
+
return `${name}:${offset}-${offset + limit}`;
|
|
44
|
+
if (offset)
|
|
45
|
+
return `${name}:${offset}+`;
|
|
46
|
+
if (limit)
|
|
47
|
+
return `${name} (${limit} lines)`;
|
|
48
|
+
return name;
|
|
49
|
+
};
|
|
50
|
+
const formatWrite = (input) => {
|
|
51
|
+
const filePath = input.file_path;
|
|
52
|
+
if (!filePath)
|
|
53
|
+
return '';
|
|
54
|
+
const name = basename(filePath);
|
|
55
|
+
const lines = countLines(input.content);
|
|
56
|
+
if (lines !== null)
|
|
57
|
+
return `${name} — ${lines} lines`;
|
|
58
|
+
return name;
|
|
59
|
+
};
|
|
60
|
+
const formatEdit = (input) => {
|
|
61
|
+
const filePath = input.file_path;
|
|
62
|
+
if (!filePath)
|
|
63
|
+
return '';
|
|
64
|
+
const name = basename(filePath);
|
|
65
|
+
const oldStr = input.old_string;
|
|
66
|
+
const newStr = input.new_string;
|
|
67
|
+
const oldLines = oldStr ? oldStr.split('\n').length : 0;
|
|
68
|
+
const newLines = newStr ? newStr.split('\n').length : 0;
|
|
69
|
+
if (oldLines || newLines)
|
|
70
|
+
return `${name} — ${oldLines}→${newLines} lines`;
|
|
71
|
+
return name;
|
|
72
|
+
};
|
|
73
|
+
const formatBash = (input) => {
|
|
74
|
+
// Prefer description field (Claude Code provides human-readable descriptions)
|
|
75
|
+
if (typeof input.description === 'string' && input.description.length > 0) {
|
|
76
|
+
return truncate(input.description, 80);
|
|
77
|
+
}
|
|
78
|
+
if (typeof input.command === 'string') {
|
|
79
|
+
// Take just the first line of multi-line commands
|
|
80
|
+
const firstLine = input.command.split('\n')[0];
|
|
81
|
+
return truncate(firstLine, 80);
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
};
|
|
85
|
+
const formatGrep = (input) => {
|
|
86
|
+
const pattern = input.pattern;
|
|
87
|
+
if (!pattern)
|
|
88
|
+
return '';
|
|
89
|
+
const glob = input.glob;
|
|
90
|
+
const path = input.path;
|
|
91
|
+
const type = input.type;
|
|
92
|
+
const parts = [truncate(pattern, 40)];
|
|
93
|
+
if (glob)
|
|
94
|
+
parts.push(`in ${glob}`);
|
|
95
|
+
else if (type)
|
|
96
|
+
parts.push(`in *.${type}`);
|
|
97
|
+
else if (path)
|
|
98
|
+
parts.push(`in ${basename(path)}`);
|
|
99
|
+
return parts.join(' ');
|
|
100
|
+
};
|
|
101
|
+
const formatGlob = (input) => {
|
|
102
|
+
const pattern = input.pattern;
|
|
103
|
+
if (!pattern)
|
|
104
|
+
return '';
|
|
105
|
+
const path = input.path;
|
|
106
|
+
if (path)
|
|
107
|
+
return `${pattern} in ${basename(path)}`;
|
|
108
|
+
return pattern;
|
|
109
|
+
};
|
|
110
|
+
const formatTask = (input) => {
|
|
111
|
+
const subagentType = input.subagent_type;
|
|
112
|
+
const description = input.description;
|
|
113
|
+
const prompt = input.prompt;
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (subagentType)
|
|
116
|
+
parts.push(`[${subagentType}]`);
|
|
117
|
+
if (description)
|
|
118
|
+
parts.push(truncate(description, 60));
|
|
119
|
+
else if (prompt)
|
|
120
|
+
parts.push(truncate(prompt, 60));
|
|
121
|
+
return parts.join(' ') || '';
|
|
122
|
+
};
|
|
123
|
+
const formatWebFetch = (input) => {
|
|
124
|
+
const url = input.url;
|
|
125
|
+
if (!url)
|
|
126
|
+
return '';
|
|
127
|
+
return hostname(url);
|
|
128
|
+
};
|
|
129
|
+
const formatWebSearch = (input) => {
|
|
130
|
+
const query = input.query;
|
|
131
|
+
if (!query)
|
|
132
|
+
return '';
|
|
133
|
+
return truncate(query, 80);
|
|
134
|
+
};
|
|
135
|
+
const formatToolSearch = (input) => {
|
|
136
|
+
const query = input.query;
|
|
137
|
+
if (!query)
|
|
138
|
+
return '';
|
|
139
|
+
return truncate(query, 80);
|
|
140
|
+
};
|
|
141
|
+
const formatNotebookEdit = (input) => {
|
|
142
|
+
const filePath = input.file_path;
|
|
143
|
+
const cellNumber = input.cell_number;
|
|
144
|
+
if (!filePath)
|
|
145
|
+
return '';
|
|
146
|
+
const name = basename(filePath);
|
|
147
|
+
if (cellNumber !== undefined)
|
|
148
|
+
return `${name} cell ${cellNumber}`;
|
|
149
|
+
return name;
|
|
150
|
+
};
|
|
151
|
+
const formatTaskCreate = (input) => {
|
|
152
|
+
const subject = input.subject;
|
|
153
|
+
if (subject)
|
|
154
|
+
return truncate(subject, 80);
|
|
155
|
+
return '';
|
|
156
|
+
};
|
|
157
|
+
const formatTaskUpdate = (input) => {
|
|
158
|
+
const taskId = input.task_id;
|
|
159
|
+
const status = input.status;
|
|
160
|
+
if (taskId && status)
|
|
161
|
+
return `${taskId} → ${status}`;
|
|
162
|
+
if (taskId)
|
|
163
|
+
return taskId;
|
|
164
|
+
return '';
|
|
165
|
+
};
|
|
166
|
+
// ── Registry ──
|
|
167
|
+
const TOOL_FORMATTERS = {
|
|
168
|
+
Read: formatRead,
|
|
169
|
+
Write: formatWrite,
|
|
170
|
+
Edit: formatEdit,
|
|
171
|
+
Bash: formatBash,
|
|
172
|
+
Grep: formatGrep,
|
|
173
|
+
Glob: formatGlob,
|
|
174
|
+
Task: formatTask,
|
|
175
|
+
WebFetch: formatWebFetch,
|
|
176
|
+
WebSearch: formatWebSearch,
|
|
177
|
+
ToolSearch: formatToolSearch,
|
|
178
|
+
NotebookEdit: formatNotebookEdit,
|
|
179
|
+
TaskCreate: formatTaskCreate,
|
|
180
|
+
TaskUpdate: formatTaskUpdate,
|
|
181
|
+
};
|
|
182
|
+
// ── Generic fallback ──
|
|
183
|
+
function genericFallback(input) {
|
|
184
|
+
if (typeof input.command === 'string')
|
|
185
|
+
return truncate(input.command, 80);
|
|
186
|
+
if (typeof input.file_path === 'string')
|
|
187
|
+
return truncate(input.file_path, 80);
|
|
188
|
+
if (typeof input.pattern === 'string')
|
|
189
|
+
return truncate(input.pattern, 80);
|
|
190
|
+
if (typeof input.query === 'string')
|
|
191
|
+
return truncate(input.query, 80);
|
|
192
|
+
if (typeof input.path === 'string')
|
|
193
|
+
return truncate(input.path, 80);
|
|
194
|
+
if (typeof input.url === 'string')
|
|
195
|
+
return truncate(input.url, 80);
|
|
196
|
+
for (const val of Object.values(input)) {
|
|
197
|
+
if (typeof val === 'string' && val.length > 0)
|
|
198
|
+
return truncate(val, 80);
|
|
199
|
+
}
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
// ── Public API ──
|
|
203
|
+
/**
|
|
204
|
+
* Produces a contextual one-liner summary for a tool call.
|
|
205
|
+
*
|
|
206
|
+
* Uses a registry of per-tool formatters for known tools (Read, Write, Edit,
|
|
207
|
+
* Bash, Grep, Glob, Task, WebFetch, WebSearch, etc.) and falls back to
|
|
208
|
+
* generic field extraction for unknown tools.
|
|
209
|
+
*
|
|
210
|
+
* @param toolName - The canonical tool name (e.g., "Read", "Bash")
|
|
211
|
+
* @param input - The tool's input parameters
|
|
212
|
+
* @returns A human-readable one-liner summary (may be empty string)
|
|
213
|
+
*/
|
|
214
|
+
export function formatToolSummary(toolName, input) {
|
|
215
|
+
// Strip MCP prefixes: "mcp__server__tool" → use "tool" for matching
|
|
216
|
+
const baseName = toolName.includes('__') ? (toolName.split('__').pop() ?? toolName) : toolName;
|
|
217
|
+
const formatter = TOOL_FORMATTERS[toolName] ?? TOOL_FORMATTERS[baseName];
|
|
218
|
+
if (formatter) {
|
|
219
|
+
const result = formatter(input);
|
|
220
|
+
if (result)
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
return genericFallback(input);
|
|
224
|
+
}
|
|
225
|
+
// Re-export truncate for use by other formatters
|
|
226
|
+
export { truncate };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure display formatting helpers shared by CLI, extension host, webviews,
|
|
3
|
+
* and downstream apps.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally has no Node-only imports.
|
|
6
|
+
*
|
|
7
|
+
* @module formatting
|
|
8
|
+
*/
|
|
9
|
+
/** Format a token count with compact `k` / `M` suffixes. */
|
|
10
|
+
export function formatTokenCount(value, options = {}) {
|
|
11
|
+
if (!Number.isFinite(value))
|
|
12
|
+
return '0';
|
|
13
|
+
const n = Math.trunc(value);
|
|
14
|
+
const sign = n < 0 ? '-' : '';
|
|
15
|
+
const abs = Math.abs(n);
|
|
16
|
+
const thousandsSuffix = options.suffixCase === 'upper' ? 'K' : 'k';
|
|
17
|
+
if (abs >= 1_000_000)
|
|
18
|
+
return `${sign}${(abs / 1_000_000).toFixed(1)}M`;
|
|
19
|
+
if (abs >= 1_000)
|
|
20
|
+
return `${sign}${(abs / 1_000).toFixed(1)}${thousandsSuffix}`;
|
|
21
|
+
return `${n}`;
|
|
22
|
+
}
|
|
23
|
+
/** Format an elapsed duration in milliseconds. */
|
|
24
|
+
export function formatDurationMs(ms, options = {}) {
|
|
25
|
+
const invalid = options.invalid ?? 'N/A';
|
|
26
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
27
|
+
return invalid;
|
|
28
|
+
const style = options.style ?? 'spaced';
|
|
29
|
+
const separator = style === 'compact' ? '' : ' ';
|
|
30
|
+
const secondsFractionDigits = options.secondsFractionDigits ?? 0;
|
|
31
|
+
if (ms < 1000) {
|
|
32
|
+
if (options.includeMilliseconds)
|
|
33
|
+
return `${Math.round(ms)}ms`;
|
|
34
|
+
return `${(ms / 1000).toFixed(Math.max(1, secondsFractionDigits))}s`;
|
|
35
|
+
}
|
|
36
|
+
const seconds = ms / 1000;
|
|
37
|
+
if (seconds < 60) {
|
|
38
|
+
if (secondsFractionDigits > 0)
|
|
39
|
+
return `${seconds.toFixed(secondsFractionDigits)}s`;
|
|
40
|
+
return `${Math.floor(seconds)}s`;
|
|
41
|
+
}
|
|
42
|
+
const totalSeconds = Math.floor(seconds);
|
|
43
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
44
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
45
|
+
const remainingSeconds = totalSeconds % 60;
|
|
46
|
+
if (hours > 0) {
|
|
47
|
+
return `${hours}h${separator}${minutes}m${separator}${remainingSeconds}s`;
|
|
48
|
+
}
|
|
49
|
+
return `${minutes}m${separator}${remainingSeconds}s`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Calendar-date key (`YYYY-MM-DD`) in the machine's local time zone.
|
|
53
|
+
*
|
|
54
|
+
* Sidekick buckets "today", daily history, and heatmaps by the user's local
|
|
55
|
+
* day, so every writer and reader of a date key goes through this helper.
|
|
56
|
+
* A UTC key would move late-evening work onto the next day for anyone west
|
|
57
|
+
* of Greenwich and onto the previous day for anyone east of it.
|
|
58
|
+
*/
|
|
59
|
+
export function formatLocalDateKey(value = new Date()) {
|
|
60
|
+
const date = typeof value === 'number' ? new Date(value) : value;
|
|
61
|
+
if (Number.isNaN(date.getTime()))
|
|
62
|
+
return '';
|
|
63
|
+
const year = date.getFullYear();
|
|
64
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
65
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
66
|
+
return `${year}-${month}-${day}`;
|
|
67
|
+
}
|
|
68
|
+
/** Parse a `YYYY-MM-DD` key produced by `formatLocalDateKey` back into local midnight. */
|
|
69
|
+
export function parseLocalDateKey(key) {
|
|
70
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
|
|
71
|
+
if (!match)
|
|
72
|
+
return null;
|
|
73
|
+
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
74
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
75
|
+
}
|
|
76
|
+
/** Shift a `YYYY-MM-DD` key by whole local days (DST-safe). */
|
|
77
|
+
export function addLocalDays(key, days) {
|
|
78
|
+
const date = parseLocalDateKey(key);
|
|
79
|
+
if (!date)
|
|
80
|
+
return key;
|
|
81
|
+
return formatLocalDateKey(new Date(date.getFullYear(), date.getMonth(), date.getDate() + days));
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const ALLOWED_PLACEHOLDERS = new Set(['sessionId', 'provider', 'projectPath']);
|
|
2
|
+
/** Renders an external handoff URL using identifiers only. */
|
|
3
|
+
export function renderHandoffUrlTemplate(template, identifiers) {
|
|
4
|
+
if (!template.trim())
|
|
5
|
+
throw new Error('No handoff URL template is configured.');
|
|
6
|
+
const rendered = template.replace(/\{([^}]+)\}/g, (_match, name) => {
|
|
7
|
+
if (!ALLOWED_PLACEHOLDERS.has(name)) {
|
|
8
|
+
throw new Error(`Unsupported handoff URL placeholder: {${name}}`);
|
|
9
|
+
}
|
|
10
|
+
return encodeURIComponent(identifiers[name]);
|
|
11
|
+
});
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(rendered);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('The rendered handoff URL is invalid.');
|
|
18
|
+
}
|
|
19
|
+
if (['javascript:', 'data:', 'file:'].includes(url.protocol)) {
|
|
20
|
+
throw new Error(`Unsafe handoff URL protocol: ${url.protocol}`);
|
|
21
|
+
}
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|