idletime 0.1.0 → 0.1.1

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.
Binary file
package/dist/idletime.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "idletime",
5
- version: "0.1.0",
5
+ version: "0.1.1",
6
6
  description: "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
7
7
  author: "ParkerRex",
8
8
  main: "./dist/idletime.js",
@@ -1039,33 +1039,130 @@ function shortenPath(pathText, maxLength) {
1039
1039
 
1040
1040
  // src/reporting/render-theme.ts
1041
1041
  var roleStyles = {
1042
- focus: "1;38;2;56;189;248",
1043
- active: "1;38;2;96;165;250",
1044
- agent: "1;38;2;168;85;247",
1045
- idle: "1;38;2;250;204;21",
1046
- burn: "1;38;2;251;146;60",
1047
- frame: "1;38;2;125;211;252",
1048
- heading: "1;38;2;248;250;252",
1049
- muted: "38;2;148;163;184",
1050
- value: "1;38;2;226;232;240"
1042
+ focus: "1;38;2;236;239;148",
1043
+ active: "1;38;2;208;219;96",
1044
+ agent: "1;38;2;166;182;77",
1045
+ idle: "1;38;2;138;150;66",
1046
+ burn: "1;38;2;228;209;92",
1047
+ raw: "1;38;2;188;172;80",
1048
+ frame: "1;38;2;149;158;56",
1049
+ heading: "1;38;2;249;246;212",
1050
+ muted: "38;2;142;145;96",
1051
+ value: "1;38;2;242;236;179"
1051
1052
  };
1052
1053
  function createRenderOptions(shareMode) {
1053
1054
  return {
1054
1055
  colorEnabled: Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined,
1055
- shareMode
1056
+ shareMode,
1057
+ terminalWidth: process.stdout.columns ?? null
1056
1058
  };
1057
1059
  }
1058
1060
  function paint(text, role, options) {
1061
+ return paintAnsi(text, roleStyles[role], options);
1062
+ }
1063
+ function paintAnsi(text, style, options) {
1059
1064
  if (!options.colorEnabled || text.length === 0) {
1060
1065
  return text;
1061
1066
  }
1062
- return `\x1B[${roleStyles[role]}m${text}\x1B[0m`;
1067
+ return `\x1B[${style}m${text}\x1B[0m`;
1063
1068
  }
1064
1069
  function dim(text, options) {
1065
- if (!options.colorEnabled || text.length === 0) {
1066
- return text;
1070
+ return paintAnsi(text, "2", options);
1071
+ }
1072
+ function measureVisibleTextWidth(text) {
1073
+ let visibleWidth = 0;
1074
+ for (let index = 0;index < text.length; index += 1) {
1075
+ if (text[index] === "\x1B" && text[index + 1] === "[") {
1076
+ index += 2;
1077
+ while (index < text.length && text[index] !== "m") {
1078
+ index += 1;
1079
+ }
1080
+ continue;
1081
+ }
1082
+ visibleWidth += 1;
1067
1083
  }
1068
- return `\x1B[2m${text}\x1B[0m`;
1084
+ return visibleWidth;
1085
+ }
1086
+
1087
+ // src/reporting/render-logo-section.ts
1088
+ var baseBackgroundStyle = "48;2;12;15;8";
1089
+ var wordmarkStyle = `${baseBackgroundStyle};1;38;2;247;245;204`;
1090
+ var wordmarkLines = [
1091
+ " ▄▄ ▄▄",
1092
+ "▀▀ ██ ██ ██ ▀▀",
1093
+ "██ ▄████ ██ ▄█▀█▄ ▀██▀▀ ██ ███▄███▄ ▄█▀█▄",
1094
+ "██ ██ ██ ██ ██▄█▀ ██ ██ ██ ██ ██ ██▄█▀",
1095
+ "██▄ ▀████ ██ ▀█▄▄▄ ██ ██▄ ██ ██ ██ ▀█▄▄▄"
1096
+ ];
1097
+ var patternColors = [
1098
+ { red: 20, green: 24, blue: 10 },
1099
+ { red: 48, green: 58, blue: 18 },
1100
+ { red: 86, green: 96, blue: 24 },
1101
+ { red: 128, green: 138, blue: 30 },
1102
+ { red: 176, green: 188, blue: 40 },
1103
+ { red: 220, green: 228, blue: 78 }
1104
+ ];
1105
+ var monochromePatternCharacters = ["░", "░", "▒", "▓", "█"];
1106
+ function buildLogoSection(requestedWidth, options) {
1107
+ const wordmarkWidth = Math.max(...wordmarkLines.map((line) => line.length));
1108
+ const sectionWidth = Math.max(requestedWidth, wordmarkWidth);
1109
+ const patternWidth = Math.max(0, sectionWidth - wordmarkWidth);
1110
+ return wordmarkLines.map((line, rowIndex) => {
1111
+ const paddedWordmark = padRight(line, wordmarkWidth);
1112
+ const patternTail = buildPatternTail(patternWidth, rowIndex, options);
1113
+ return `${paintAnsi(paddedWordmark, wordmarkStyle, options)}${patternTail}`;
1114
+ });
1115
+ }
1116
+ function resolveLogoSectionWidth(minimumWidth, options) {
1117
+ return Math.max(minimumWidth, options.terminalWidth ?? 0);
1118
+ }
1119
+ function buildPatternTail(width, rowIndex, options) {
1120
+ if (!options.colorEnabled) {
1121
+ return buildMonochromePatternTail(width, rowIndex);
1122
+ }
1123
+ let patternTail = "";
1124
+ let currentStyle = "";
1125
+ let currentSegmentWidth = 0;
1126
+ for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1127
+ const style = getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex));
1128
+ if (style === currentStyle) {
1129
+ currentSegmentWidth += 1;
1130
+ continue;
1131
+ }
1132
+ if (currentSegmentWidth > 0) {
1133
+ patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1134
+ }
1135
+ currentStyle = style;
1136
+ currentSegmentWidth = 1;
1137
+ }
1138
+ if (currentSegmentWidth > 0) {
1139
+ patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1140
+ }
1141
+ return patternTail;
1142
+ }
1143
+ function buildMonochromePatternTail(width, rowIndex) {
1144
+ let patternTail = "";
1145
+ for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1146
+ const intensity = getPatternIntensity(width, rowIndex, columnIndex);
1147
+ const characterIndex = Math.min(monochromePatternCharacters.length - 1, Math.floor(intensity * monochromePatternCharacters.length));
1148
+ patternTail += monochromePatternCharacters[characterIndex];
1149
+ }
1150
+ return patternTail;
1151
+ }
1152
+ function getPatternIntensity(width, rowIndex, columnIndex) {
1153
+ const normalizedColumn = width <= 1 ? 0 : columnIndex / Math.max(1, width - 1);
1154
+ const envelope = 0.18 + 0.82 * Math.pow(normalizedColumn, 0.82);
1155
+ const wave = Math.sin((columnIndex + rowIndex * 1.9) / 2.9) * 0.22 + Math.cos((columnIndex - rowIndex * 2.7) / 6.3) * 0.18 + Math.sin((columnIndex + rowIndex * 3.4) / 10.5) * 0.12;
1156
+ const blockOffset = ((Math.floor(columnIndex / 2) + rowIndex) % 2 === 0 ? 0.06 : -0.04) + (rowIndex === 0 ? -0.06 : 0);
1157
+ return clamp(envelope + wave + blockOffset, 0, 1);
1158
+ }
1159
+ function getPatternCellStyle(intensity) {
1160
+ const colorIndex = Math.min(patternColors.length - 1, Math.floor(intensity * patternColors.length));
1161
+ const color = patternColors[colorIndex];
1162
+ return `48;2;${color.red};${color.green};${color.blue}`;
1163
+ }
1164
+ function clamp(value, minValue, maxValue) {
1165
+ return Math.min(maxValue, Math.max(minValue, value));
1069
1166
  }
1070
1167
 
1071
1168
  // src/reporting/render-layout.ts
@@ -1095,32 +1192,39 @@ function renderSectionTitle(title, options) {
1095
1192
  }
1096
1193
 
1097
1194
  // src/reporting/render-rhythm-section.ts
1195
+ var groupSize = 4;
1098
1196
  function buildRhythmSection(report, options) {
1099
1197
  const quietValues = report.buckets.map((bucket) => Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs));
1100
1198
  const idleValues = report.hasWakeWindow ? report.buckets.map((bucket) => bucket.awakeIdleMs) : quietValues;
1101
1199
  const idleLabel = report.hasWakeWindow ? "idle" : "quiet";
1102
- return [
1200
+ const idleTotal = formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0));
1201
+ const lines = [
1103
1202
  ...renderSectionTitle("24h Rhythm", options),
1104
1203
  paint(` hours ${buildHourMarkerLine(report)}`, "muted", options),
1105
- renderRhythmRow("focus", buildSparkline(report.buckets.map((bucket) => bucket.engagedMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
1106
- renderRhythmRow("active", buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs)), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options),
1107
- renderRhythmRow(padRight(idleLabel, 6).trimEnd(), buildSparkline(idleValues), formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0)), "idle", options),
1108
- renderRhythmRow("burn", buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn)), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options)
1204
+ renderRhythmRow("focus", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.engagedMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
1205
+ renderRhythmRow("active", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options)
1109
1206
  ];
1207
+ lines.push(renderRhythmRow(padRight(idleLabel, 6).trimEnd(), buildGroupedTrack(buildSparkline(idleValues)), idleTotal, "idle", options));
1208
+ lines.push(renderRhythmRow("burn", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn))), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options));
1209
+ return lines;
1210
+ }
1211
+ function buildGroupedTrack(text) {
1212
+ const groups = [];
1213
+ for (let i = 0;i < text.length; i += groupSize) {
1214
+ groups.push(text.slice(i, i + groupSize));
1215
+ }
1216
+ return groups.join("│");
1110
1217
  }
1111
1218
  function buildHourMarkerLine(report) {
1112
- const markerCharacters = Array.from({ length: report.buckets.length }, () => " ");
1113
- for (const [index, bucket] of report.buckets.entries()) {
1114
- if (index % 4 !== 0) {
1219
+ const markerGroups = [];
1220
+ for (let index = 0;index < report.buckets.length; index += groupSize) {
1221
+ const bucket = report.buckets[index];
1222
+ if (!bucket) {
1115
1223
  continue;
1116
1224
  }
1117
- const hourLabel = formatHourOfDay(bucket.start, report.window);
1118
- markerCharacters[index] = hourLabel[0] ?? " ";
1119
- if (index + 1 < markerCharacters.length) {
1120
- markerCharacters[index + 1] = hourLabel[1] ?? " ";
1121
- }
1225
+ markerGroups.push(padRight(formatHourOfDay(bucket.start, report.window), Math.min(groupSize, report.buckets.length - index)));
1122
1226
  }
1123
- return markerCharacters.join("");
1227
+ return markerGroups.join("");
1124
1228
  }
1125
1229
  function renderRhythmRow(label, sparkline, totalText, role, options) {
1126
1230
  return `${paint(` ${padRight(label, 6)}`, role, options)} ${paint(sparkline, role, options)} ${paint(totalText, "value", options)}`;
@@ -1147,11 +1251,16 @@ function renderFullHourlyReport(report, options) {
1147
1251
  const lines = [];
1148
1252
  const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1149
1253
  const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
1150
- lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
1254
+ const panelLines = renderPanel(`idletime hourly • ${report.window.label}`, [
1151
1255
  `${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
1152
1256
  buildFilterLine(report),
1153
1257
  `peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourBucketLabel(peakFocusBucket.start, report.window)} • concurrency ${Math.max(...report.buckets.map((bucket) => bucket.peakConcurrentAgents), 0)}`
1154
- ], options));
1258
+ ], options);
1259
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1260
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1261
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1262
+ lines.push("");
1263
+ lines.push(...panelLines);
1155
1264
  lines.push("");
1156
1265
  lines.push(...buildRhythmSection(report, options));
1157
1266
  lines.push("");
@@ -1171,11 +1280,16 @@ function renderFullHourlyReport(report, options) {
1171
1280
  function renderShareHourlyReport(report, options) {
1172
1281
  const lines = [];
1173
1282
  const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1174
- lines.push(...renderPanel(`idletime hourly • ${report.window.label}`, [
1283
+ const panelLines = renderPanel(`idletime hourly • ${report.window.label}`, [
1175
1284
  `${formatTimestamp(report.window.start, report.window)} -> ${formatTimestamp(report.window.end, report.window)}`,
1176
1285
  buildFilterLine(report),
1177
1286
  `peak burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourBucketLabel(peakBurnBucket.start, report.window)}`
1178
- ], options));
1287
+ ], options);
1288
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1289
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1290
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1291
+ lines.push("");
1292
+ lines.push(...panelLines);
1179
1293
  lines.push("");
1180
1294
  lines.push(...buildRhythmSection(report, options));
1181
1295
  lines.push("");
@@ -1324,19 +1438,18 @@ function renderFullSummaryReport(report, options, hourlyReport) {
1324
1438
  const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
1325
1439
  const headerLines = [
1326
1440
  formatTimeRange(report.window.start, report.window.end, report.window),
1327
- report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
1328
- buildHeaderMeta(report),
1441
+ `${report.sessionCounts.total} sessions · ${formatDurationHours(requestedMetrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1329
1442
  ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1330
1443
  ];
1331
- if (hourlyReport) {
1332
- headerLines.push(buildPeakLine(hourlyReport));
1333
- }
1334
- lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
1444
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1445
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1446
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1447
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1448
+ lines.push("");
1449
+ lines.push(...panelLines);
1335
1450
  if (hourlyReport) {
1336
1451
  lines.push("");
1337
1452
  lines.push(...buildRhythmSection(hourlyReport, options));
1338
- lines.push("");
1339
- lines.push(...buildSpikeSection(hourlyReport, options));
1340
1453
  }
1341
1454
  lines.push("");
1342
1455
  lines.push(...renderSectionTitle("Activity", options));
@@ -1358,10 +1471,10 @@ function renderFullSummaryReport(report, options, hourlyReport) {
1358
1471
  lines.push(...renderSectionTitle("Tokens", options));
1359
1472
  const maxBurnValue = Math.max(report.tokenTotals.practicalBurn, report.directTokenTotals.practicalBurn);
1360
1473
  const maxRawValue = Math.max(report.tokenTotals.rawTotalTokens, report.directTokenTotals.rawTotalTokens);
1361
- lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options));
1362
- lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "", "burn", options));
1363
- lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options));
1364
- lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "", "burn", options));
1474
+ lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options, "burn"));
1475
+ lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "", "raw", options, "raw"));
1476
+ lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options, "burn"));
1477
+ lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "", "raw", options, "raw"));
1365
1478
  if (report.wakeSummary) {
1366
1479
  lines.push("");
1367
1480
  lines.push(...renderSectionTitle("Wake Window", options));
@@ -1386,19 +1499,18 @@ function renderShareSummaryReport(report, options, hourlyReport) {
1386
1499
  const lines = [];
1387
1500
  const headerLines = [
1388
1501
  formatTimeRange(report.window.start, report.window.end, report.window),
1389
- report.activityWindow ? `active ${formatTimeRange(report.activityWindow.start, report.activityWindow.end, report.window)}` : "active no matching sessions",
1390
- buildHeaderMeta(report),
1502
+ `${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
1391
1503
  ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
1392
1504
  ];
1393
- if (hourlyReport) {
1394
- headerLines.push(buildPeakLine(hourlyReport));
1395
- }
1396
- lines.push(...renderPanel(`idletime • ${report.window.label}`, headerLines, options));
1505
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
1506
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
1507
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
1508
+ lines.push(...buildLogoSection(logoSectionWidth, options));
1509
+ lines.push("");
1510
+ lines.push(...panelLines);
1397
1511
  if (hourlyReport) {
1398
1512
  lines.push("");
1399
1513
  lines.push(...buildRhythmSection(hourlyReport, options));
1400
- lines.push("");
1401
- lines.push(...buildSpikeSection(hourlyReport, options));
1402
1514
  }
1403
1515
  lines.push("");
1404
1516
  lines.push(...renderSectionTitle("Snapshot", options));
@@ -1430,16 +1542,8 @@ function formatAppliedFilters(report) {
1430
1542
  function formatDurationLabel(durationMs) {
1431
1543
  return `${Math.round(durationMs / 60000)}m`;
1432
1544
  }
1433
- function buildHeaderMeta(report) {
1434
- return `${report.sessionCounts.total} sessions ${report.sessionCounts.direct} direct ${report.sessionCounts.subagent} subagent peak ${report.metrics.peakConcurrentAgents} agents`;
1435
- }
1436
- function buildPeakLine(report) {
1437
- const peakBurnBucket = report.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, report.buckets[0]);
1438
- const peakFocusBucket = report.buckets.reduce((currentPeak, bucket) => bucket.engagedMs > currentPeak.engagedMs ? bucket : currentPeak, report.buckets[0]);
1439
- return `peaks burn ${formatCompactInteger(peakBurnBucket.practicalBurn)} @ ${formatHourOfDay(peakBurnBucket.start, report.window)} • focus ${formatDurationCompact(peakFocusBucket.engagedMs)} @ ${formatHourOfDay(peakFocusBucket.start, report.window)}`;
1440
- }
1441
- function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options) {
1442
- return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), "value", options)} ${dim(detailText, options)}`;
1545
+ function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options, valueRole = "value") {
1546
+ return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), valueRole, options)} ${dim(detailText, options)}`;
1443
1547
  }
1444
1548
  function renderSnapshotRow(label, primaryText, detailText, role, options) {
1445
1549
  return `${paint(padRight(` ${label}`, 12), role, options)} ${paint(padRight(primaryText, 10), "value", options)} ${dim(detailText, options)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idletime",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
5
5
  "author": "ParkerRex",
6
6
  "main": "./dist/idletime.js",