opencode-token-tracker 1.6.3 → 1.6.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.
@@ -905,67 +905,117 @@ function getTrendValue(point, metric) {
905
905
  function formatTrendValue(value, metric) {
906
906
  return metric === "tokens" ? formatTokens(value) : metric === "messages" ? String(Math.round(value)) : formatCost(value);
907
907
  }
908
+ function formatSignedTrendValue(value, metric) {
909
+ const sign = value > 0 ? "+" : value < 0 ? "-" : "";
910
+ return `${sign}${formatTrendValue(Math.abs(value), metric)}`;
911
+ }
908
912
  function metricLabel(metric) {
909
913
  return metric === "tokens" ? "Token Trend" : metric === "messages" ? "Message Trend" : "Cost Trend";
910
914
  }
911
- function createTrendRows(height, width, gridRows) {
912
- const rows = [];
913
- for (let row = 0; row < height; row++) {
914
- const grid = gridRows.has(row);
915
- rows.push(Array.from({ length: width }, (_, x) => grid && x % 2 === 0 ? "·" : " "));
916
- }
917
- return rows;
918
- }
919
- function drawTrendSegment(rows, from, to) {
920
- const startX = Math.min(from.x, to.x);
921
- const endX = Math.max(from.x, to.x);
922
- const slope = to.y - from.y;
923
- for (let x = startX; x <= endX; x++) {
924
- const t = x === from.x ? 0 : (x - from.x) / (to.x - from.x);
925
- const y = Math.round(from.y + slope * t);
926
- const char = slope > 0 ? "╱" : slope < 0 ? "╲" : "─";
927
- rows[y][x] = char;
915
+ function buildLineRows(points, width, height) {
916
+ const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
917
+ // 1. Calculate the exact integer row r[x] for every column x from 0 to width - 1
918
+ const r = Array.from({ length: width }, () => 0);
919
+ for (let x = 0; x < width; x++) {
920
+ // Find which segment p[i] -> p[i+1] contains x
921
+ let i = 0;
922
+ for (; i < points.length - 1; i++) {
923
+ if (x >= points[i].x && x <= points[i + 1].x) {
924
+ break;
925
+ }
926
+ }
927
+ // Clamp to valid index
928
+ if (i >= points.length - 1) {
929
+ i = points.length - 2;
930
+ }
931
+ const p1 = points[i];
932
+ const p2 = points[i + 1];
933
+ if (p1.x === p2.x) {
934
+ r[x] = p1.y;
935
+ }
936
+ else {
937
+ const t = (x - p1.x) / (p2.x - p1.x);
938
+ const y = p1.y + (p2.y - p1.y) * t;
939
+ r[x] = Math.round(y);
940
+ }
928
941
  }
929
- }
930
- function drawTrendArea(rows, points) {
931
- for (let i = 1; i < points.length; i++) {
932
- const from = points[i - 1];
933
- const to = points[i];
934
- const startX = Math.min(from.x, to.x);
935
- const endX = Math.max(from.x, to.x);
936
- for (let x = startX; x <= endX; x++) {
937
- const t = x === from.x ? 0 : (x - from.x) / (to.x - from.x);
938
- const y = Math.round(from.y + (to.y - from.y) * t);
939
- for (let fillY = 0; fillY < y; fillY++) {
940
- if (rows[fillY][x] === " " || rows[fillY][x] === "·") {
941
- rows[fillY][x] = "";
942
+ // 2. Plot the line characters based on r[x] and r[x+1]
943
+ for (let x = 0; x < width; x++) {
944
+ const rCurr = r[x];
945
+ if (x === width - 1) {
946
+ // Last column has no next column, draw horizontal line
947
+ if (grid[rCurr][x] === " ") {
948
+ grid[rCurr][x] = "─";
949
+ }
950
+ break;
951
+ }
952
+ const rNext = r[x + 1];
953
+ if (rCurr === rNext) {
954
+ if (grid[rCurr][x] === " ") {
955
+ grid[rCurr][x] = "─";
956
+ }
957
+ }
958
+ else if (rCurr < rNext) {
959
+ // Going down (larger row index)
960
+ if (grid[rCurr][x] === " ") {
961
+ grid[rCurr][x] = "┐";
962
+ }
963
+ for (let y = rCurr + 1; y < rNext; y++) {
964
+ if (grid[y][x] === " ") {
965
+ grid[y][x] = "│";
942
966
  }
943
967
  }
968
+ if (grid[rNext][x] === " ") {
969
+ grid[rNext][x] = "└";
970
+ }
971
+ }
972
+ else {
973
+ // Going up (smaller row index)
974
+ if (grid[rCurr][x] === " ") {
975
+ grid[rCurr][x] = "┘";
976
+ }
977
+ for (let y = rNext + 1; y < rCurr; y++) {
978
+ if (grid[y][x] === " ") {
979
+ grid[y][x] = "│";
980
+ }
981
+ }
982
+ if (grid[rNext][x] === " ") {
983
+ grid[rNext][x] = "┌";
984
+ }
944
985
  }
945
986
  }
987
+ return grid;
946
988
  }
947
- function drawTrendPoints(rows, points) {
948
- for (const point of points) {
949
- rows[point.y][point.x] = "●";
950
- }
951
- }
989
+ const ESC = "\x1b";
990
+ const RESET = `${ESC}[0m`;
991
+ const BOLD = `${ESC}[1m`;
992
+ const DIM = `${ESC}[2m`;
993
+ const GRAY = `${ESC}[90m`;
952
994
  function buildTrendXAxis(points, chartPoints, chartWidth) {
953
995
  const labelChars = Array.from({ length: chartWidth }, () => " ");
996
+ const tickChars = Array.from({ length: chartWidth }, () => "─");
954
997
  const labelStep = Math.max(1, Math.ceil(points.length / 6));
955
998
  for (let i = 0; i < chartPoints.length; i++) {
956
999
  if (i % labelStep !== 0 && i !== chartPoints.length - 1)
957
1000
  continue;
958
1001
  const date = new Date(points[i][0]);
959
1002
  const label = `${date.getMonth() + 1}/${date.getDate()}`;
960
- const start = Math.min(Math.max(0, chartPoints[i].x - Math.floor(label.length / 2)), Math.max(0, chartWidth - label.length));
1003
+ const centerOfLabel = chartPoints[i].x;
1004
+ const start = Math.min(Math.max(0, centerOfLabel - Math.floor(label.length / 2)), Math.max(0, chartWidth - label.length));
961
1005
  const hasSpace = labelChars.slice(Math.max(0, start - 1), Math.min(chartWidth, start + label.length + 1)).every((c) => c === " ");
962
1006
  if (!hasSpace)
963
1007
  continue;
964
1008
  for (let j = 0; j < label.length; j++) {
965
1009
  labelChars[start + j] = label[j];
966
1010
  }
1011
+ if (centerOfLabel < chartWidth) {
1012
+ tickChars[centerOfLabel] = "┬";
1013
+ }
967
1014
  }
968
- return labelChars.join("");
1015
+ return {
1016
+ axisTicks: tickChars.join(""),
1017
+ axisLabels: labelChars.join("")
1018
+ };
969
1019
  }
970
1020
  function cmdTrend(flags) {
971
1021
  const days = parseInt(String(flagValue(flags, "days") ?? "30"), 10);
@@ -1001,10 +1051,16 @@ function cmdTrend(flags) {
1001
1051
  const values = sorted.map(([, d]) => getTrendValue(d, metric));
1002
1052
  const maxVal = Math.max(...values, 1);
1003
1053
  const minVal = Math.min(...values);
1054
+ const valRange = maxVal - minVal;
1055
+ // Adaptive scaling padding (e.g. 5% of range) to show beautiful ups & downs
1056
+ const pad = valRange === 0 ? maxVal * 0.1 : valRange * 0.05;
1057
+ const chartMax = maxVal + pad;
1058
+ const chartMin = Math.max(0, minVal - pad);
1059
+ const chartRange = chartMax - chartMin || 1;
1004
1060
  const totalVal = values.reduce((sum, value) => sum + value, 0);
1005
1061
  const avgVal = totalVal / values.length;
1006
1062
  const deltaVal = values[values.length - 1] - values[0];
1007
- const H = Math.max(5, Math.min(Math.floor(width / 3), 20));
1063
+ const chartHeight = sorted.length <= 3 ? 6 : Math.max(6, Math.min(Math.floor(width / 4), 12));
1008
1064
  if (width < 35) {
1009
1065
  // Fallback: simple sparkline
1010
1066
  const chars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
@@ -1013,37 +1069,48 @@ function cmdTrend(flags) {
1013
1069
  return;
1014
1070
  }
1015
1071
  const chartWidth = Math.max(width - 12, 20);
1016
- const yLabelStep = Math.max(1, Math.floor(H / 5));
1017
- const gridRows = new Set();
1018
- for (let row = 0; row < H; row++) {
1019
- if (row === 0 || row === H - 1 || (H - 1 - row) % yLabelStep === 0) {
1020
- gridRows.add(row);
1021
- }
1022
- }
1072
+ const yLabelStep = Math.max(1, Math.floor(chartHeight / 4));
1023
1073
  const chartPoints = values.map((value, i) => ({
1024
1074
  x: values.length === 1 ? Math.floor(chartWidth / 2) : Math.round((i / (values.length - 1)) * (chartWidth - 1)),
1025
- y: Math.round((value / maxVal) * (H - 1)),
1075
+ y: Math.max(0, chartHeight - 1 - Math.round(((value - chartMin) / chartRange) * (chartHeight - 1))),
1026
1076
  }));
1027
- const rows = createTrendRows(H, chartWidth, gridRows);
1028
- drawTrendArea(rows, chartPoints);
1029
- for (let i = 1; i < chartPoints.length; i++) {
1030
- drawTrendSegment(rows, chartPoints[i - 1], chartPoints[i]);
1077
+ const grid = buildLineRows(chartPoints, chartWidth, chartHeight);
1078
+ // Metric premium color themes
1079
+ let metricColor = "\x1b[36m"; // default Cyan
1080
+ if (metric === "cost") {
1081
+ metricColor = "\x1b[32m"; // Green
1082
+ }
1083
+ else if (metric === "messages") {
1084
+ metricColor = "\x1b[33m"; // Yellow
1085
+ }
1086
+ const peakStr = `${metricColor}${BOLD}${formatTrendValue(maxVal, metric)}${RESET}`;
1087
+ const avgStr = `${metricColor}${BOLD}${formatTrendValue(avgVal, metric)}${RESET}`;
1088
+ let deltaColor = "\x1b[32m"; // Green
1089
+ if (metric === "cost") {
1090
+ deltaColor = deltaVal > 0 ? "\x1b[31m" : "\x1b[32m"; // Red for cost increase, Green for decrease
1091
+ }
1092
+ else {
1093
+ deltaColor = deltaVal > 0 ? "\x1b[32m" : "\x1b[31m"; // Green for token/msg increase, Red for decrease
1031
1094
  }
1032
- drawTrendPoints(rows, chartPoints);
1095
+ const deltaStr = `${deltaColor}${BOLD}${formatSignedTrendValue(deltaVal, metric)}${RESET}`;
1033
1096
  const lines = [];
1034
- lines.push(`${metricLabel(metric)} · ${sorted.length} days · peak ${formatTrendValue(maxVal, metric)} · avg ${formatTrendValue(avgVal, metric)} · Δ ${deltaVal >= 0 ? "+" : ""}${formatTrendValue(deltaVal, metric)}`);
1035
- lines.push(`range ${formatTrendValue(minVal, metric)} → ${formatTrendValue(maxVal, metric)}`);
1036
- for (let row = H - 1; row >= 0; row--) {
1037
- const valAtRow = (row / (H - 1)) * maxVal;
1038
- const label = gridRows.has(row)
1039
- ? formatTrendValue(valAtRow, metric)
1040
- : "";
1041
- const line = `${padLeft(label, 9)}${row === 0 ? " ┼" : " ┤"}${rows[row].join("")}`;
1097
+ lines.push(`${metricColor}${BOLD}${metricLabel(metric)}${RESET} · ${sorted.length} days · peak ${peakStr} · avg ${avgStr} · Δ ${deltaStr}`);
1098
+ lines.push(`${GRAY}range ${formatTrendValue(minVal, metric)} → ${formatTrendValue(maxVal, metric)}${RESET}`);
1099
+ // Colorize the line chart points and connection characters
1100
+ const colorRows = grid.map(row => row.map(char => char === " " ? " " : `${metricColor}${char}${RESET}`).join(""));
1101
+ const { axisTicks, axisLabels } = buildTrendXAxis(sorted, chartPoints, chartWidth);
1102
+ for (let row = 0; row < chartHeight; row++) {
1103
+ const valueRatio = 1 - row / (chartHeight - 1);
1104
+ const valAtRow = chartMin + valueRatio * chartRange;
1105
+ const hasLabel = row === 0 || row === chartHeight - 1 || row % yLabelStep === 0;
1106
+ const label = hasLabel ? formatTrendValue(valAtRow, metric) : "";
1107
+ const tick = hasLabel ? "┤" : "│";
1108
+ const line = `${padLeft(label, 9)} ${GRAY}${tick}${RESET}${colorRows[row]}`;
1042
1109
  lines.push(line);
1043
1110
  }
1044
- const axis = `${" ".repeat(9)} └${"─".repeat(chartWidth)}`;
1111
+ const axis = `${" ".repeat(9)} ${GRAY}└${axisTicks}${RESET}`;
1045
1112
  lines.push(axis);
1046
- lines.push(`${" ".repeat(11)}${buildTrendXAxis(sorted, chartPoints, chartWidth)}`);
1113
+ lines.push(`${" ".repeat(11)}${GRAY}${axisLabels}${RESET}`);
1047
1114
  console.log();
1048
1115
  for (const l of lines)
1049
1116
  console.log(` ${l}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Real-time token usage and cost tracking plugin for OpenCode with Toast notifications and CLI stats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",