cc-context-stats 1.7.0 → 1.8.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.
Files changed (102) hide show
  1. package/package.json +9 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/scripts/statusline.js +128 -18
  4. package/.editorconfig +0 -60
  5. package/.eslintrc.json +0 -35
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  8. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  9. package/.github/dependabot.yml +0 -44
  10. package/.github/workflows/ci.yml +0 -294
  11. package/.github/workflows/release.yml +0 -151
  12. package/.pre-commit-config.yaml +0 -74
  13. package/.prettierrc +0 -33
  14. package/.shellcheckrc +0 -10
  15. package/CHANGELOG.md +0 -163
  16. package/CLAUDE.md +0 -66
  17. package/CODE_OF_CONDUCT.md +0 -59
  18. package/CONTRIBUTING.md +0 -240
  19. package/RELEASE_NOTES.md +0 -19
  20. package/SECURITY.md +0 -44
  21. package/TODOS.md +0 -72
  22. package/assets/logo/favicon.svg +0 -19
  23. package/assets/logo/logo-black.svg +0 -24
  24. package/assets/logo/logo-full.svg +0 -40
  25. package/assets/logo/logo-icon.svg +0 -27
  26. package/assets/logo/logo-mark.svg +0 -28
  27. package/assets/logo/logo-white.svg +0 -24
  28. package/assets/logo/logo-wordmark.svg +0 -6
  29. package/config/settings-example.json +0 -7
  30. package/config/settings-node.json +0 -7
  31. package/config/settings-python.json +0 -7
  32. package/docs/ARCHITECTURE.md +0 -128
  33. package/docs/CSV_FORMAT.md +0 -42
  34. package/docs/DEPLOYMENT.md +0 -71
  35. package/docs/DEVELOPMENT.md +0 -161
  36. package/docs/configuration.md +0 -118
  37. package/docs/context-stats.md +0 -143
  38. package/docs/installation.md +0 -255
  39. package/docs/scripts.md +0 -140
  40. package/docs/troubleshooting.md +0 -278
  41. package/images/claude-statusline-token-graph.gif +0 -0
  42. package/images/claude-statusline.png +0 -0
  43. package/images/context-status-dumbzone.png +0 -0
  44. package/images/context-status.png +0 -0
  45. package/images/statusline-detail.png +0 -0
  46. package/images/token-graph.jpeg +0 -0
  47. package/images/token-graph.png +0 -0
  48. package/images/v1.6.1.png +0 -0
  49. package/install +0 -351
  50. package/install.sh +0 -298
  51. package/jest.config.js +0 -11
  52. package/pyproject.toml +0 -115
  53. package/requirements-dev.txt +0 -12
  54. package/scripts/statusline-full.sh +0 -304
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -485
  58. package/src/claude_statusline/__init__.py +0 -11
  59. package/src/claude_statusline/__main__.py +0 -6
  60. package/src/claude_statusline/cli/__init__.py +0 -1
  61. package/src/claude_statusline/cli/context_stats.py +0 -512
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -169
  64. package/src/claude_statusline/core/__init__.py +0 -1
  65. package/src/claude_statusline/core/colors.py +0 -124
  66. package/src/claude_statusline/core/config.py +0 -148
  67. package/src/claude_statusline/core/git.py +0 -78
  68. package/src/claude_statusline/core/state.py +0 -323
  69. package/src/claude_statusline/formatters/__init__.py +0 -1
  70. package/src/claude_statusline/formatters/layout.py +0 -67
  71. package/src/claude_statusline/formatters/time.py +0 -50
  72. package/src/claude_statusline/formatters/tokens.py +0 -70
  73. package/src/claude_statusline/graphs/__init__.py +0 -1
  74. package/src/claude_statusline/graphs/renderer.py +0 -366
  75. package/src/claude_statusline/graphs/statistics.py +0 -92
  76. package/src/claude_statusline/ui/__init__.py +0 -1
  77. package/src/claude_statusline/ui/icons.py +0 -93
  78. package/src/claude_statusline/ui/waiting.py +0 -62
  79. package/tests/bash/test_delta_parity.bats +0 -199
  80. package/tests/bash/test_install.bats +0 -29
  81. package/tests/bash/test_parity.bats +0 -315
  82. package/tests/bash/test_statusline_full.bats +0 -139
  83. package/tests/bash/test_statusline_git.bats +0 -42
  84. package/tests/bash/test_statusline_minimal.bats +0 -37
  85. package/tests/fixtures/json/comma_in_path.json +0 -31
  86. package/tests/fixtures/json/high_usage.json +0 -17
  87. package/tests/fixtures/json/low_usage.json +0 -17
  88. package/tests/fixtures/json/medium_usage.json +0 -17
  89. package/tests/fixtures/json/valid_full.json +0 -30
  90. package/tests/fixtures/json/valid_minimal.json +0 -9
  91. package/tests/node/rotation.test.js +0 -89
  92. package/tests/node/statusline.test.js +0 -240
  93. package/tests/python/conftest.py +0 -84
  94. package/tests/python/test_colors.py +0 -105
  95. package/tests/python/test_config_colors.py +0 -78
  96. package/tests/python/test_data_pipeline.py +0 -446
  97. package/tests/python/test_explain.py +0 -177
  98. package/tests/python/test_icons.py +0 -152
  99. package/tests/python/test_layout.py +0 -127
  100. package/tests/python/test_state_rotation_validation.py +0 -232
  101. package/tests/python/test_statusline.py +0 -215
  102. package/tests/python/test_waiting.py +0 -127
@@ -1,366 +0,0 @@
1
- """ASCII graph rendering engine."""
2
-
3
- from __future__ import annotations
4
-
5
- import shutil
6
- from dataclasses import dataclass
7
-
8
- from claude_statusline.core.colors import ColorManager
9
- from claude_statusline.formatters.time import format_duration, format_timestamp
10
- from claude_statusline.formatters.tokens import format_tokens
11
- from claude_statusline.graphs.statistics import calculate_deltas, calculate_stats
12
-
13
-
14
- @dataclass
15
- class GraphDimensions:
16
- """Terminal and graph dimensions."""
17
-
18
- term_width: int
19
- term_height: int
20
- graph_width: int
21
- graph_height: int
22
-
23
- @classmethod
24
- def detect(cls) -> GraphDimensions:
25
- """Detect terminal dimensions and calculate graph size."""
26
- term_size = shutil.get_terminal_size((80, 24))
27
- term_width = term_size.columns
28
- term_height = term_size.lines
29
-
30
- # Calculate graph dimensions
31
- graph_width = term_width - 15 # Reserve space for Y-axis labels
32
- graph_height = term_height // 3 # Each graph takes 1/3 of terminal
33
-
34
- # Enforce minimums and maximums
35
- graph_width = max(30, graph_width)
36
- graph_height = max(8, min(20, graph_height))
37
-
38
- return cls(
39
- term_width=term_width,
40
- term_height=term_height,
41
- graph_width=graph_width,
42
- graph_height=graph_height,
43
- )
44
-
45
-
46
- class GraphRenderer:
47
- """ASCII graph rendering engine."""
48
-
49
- # Characters for graph rendering
50
- DOT = "●"
51
- FILL_LIGHT = "▒"
52
- FILL_DARK = "░"
53
-
54
- def __init__(
55
- self,
56
- colors: ColorManager | None = None,
57
- dimensions: GraphDimensions | None = None,
58
- token_detail: bool = True,
59
- ) -> None:
60
- """Initialize graph renderer.
61
-
62
- Args:
63
- colors: ColorManager instance. Creates default if None.
64
- dimensions: GraphDimensions instance. Detects if None.
65
- token_detail: Whether to show detailed token counts.
66
- """
67
- self.colors = colors or ColorManager(enabled=True)
68
- self.dimensions = dimensions or GraphDimensions.detect()
69
- self.token_detail = token_detail
70
- self._output_lines: list[str] | None = None
71
-
72
- def begin_buffering(self) -> None:
73
- """Start buffering output instead of printing directly."""
74
- self._output_lines = []
75
-
76
- def get_buffer(self) -> str:
77
- """Return buffered output as a single string and stop buffering."""
78
- if self._output_lines is None:
79
- return ""
80
- result = "\n".join(self._output_lines)
81
- self._output_lines = None
82
- return result
83
-
84
- def _emit(self, line: str = "") -> None:
85
- """Emit a line of output. Buffers if buffering is active, otherwise prints."""
86
- if self._output_lines is not None:
87
- self._output_lines.append(line)
88
- else:
89
- print(line)
90
-
91
- def render_timeseries(
92
- self,
93
- data: list[int],
94
- timestamps: list[int],
95
- title: str,
96
- color: str,
97
- ) -> None:
98
- """Render a timeseries ASCII graph.
99
-
100
- Args:
101
- data: List of values to plot
102
- timestamps: Corresponding timestamps for x-axis labels
103
- title: Graph title
104
- color: ANSI color code for the graph
105
- """
106
- n = len(data)
107
- if n == 0:
108
- return
109
-
110
- stats = calculate_stats(data)
111
- min_val = stats.min_val
112
- max_val = stats.max_val
113
-
114
- # Avoid division by zero
115
- if min_val == max_val:
116
- max_val = min_val + 1
117
- value_range = max_val - min_val
118
-
119
- width = self.dimensions.graph_width
120
- height = self.dimensions.graph_height
121
-
122
- # Print title and stats
123
- self._emit()
124
- self._emit(f"{self.colors.bold}{title}{self.colors.reset}")
125
- self._emit(
126
- f"{self.colors.dim}Max: {format_tokens(max_val, self.token_detail)} "
127
- f"Min: {format_tokens(min_val, self.token_detail)} "
128
- f"Points: {n}{self.colors.reset}"
129
- )
130
- self._emit()
131
-
132
- # Build the graph grid
133
- grid = self._build_grid(data, min_val, max_val, value_range, width, height)
134
-
135
- # Print grid with Y-axis labels
136
- for r in range(height):
137
- val = max_val - r * value_range // (height - 1)
138
-
139
- # Show labels at top, middle, and bottom
140
- if r == 0 or r == height // 2 or r == height - 1:
141
- label = format_tokens(val, self.token_detail)
142
- else:
143
- label = ""
144
-
145
- row_data = grid[r] if r < len(grid) else " " * width
146
- self._emit(
147
- f"{label:>10} {self.colors.dim}│{self.colors.reset}"
148
- f"{color}{row_data}{self.colors.reset}"
149
- )
150
-
151
- # X-axis
152
- self._emit(f"{'':>10} {self.colors.dim}└{'─' * width}{self.colors.reset}")
153
-
154
- # Time labels
155
- if timestamps:
156
- first_time = format_timestamp(timestamps[0])
157
- last_time = format_timestamp(timestamps[-1])
158
- mid_idx = (n - 1) // 2
159
- mid_time = format_timestamp(timestamps[mid_idx]) if n > 2 else ""
160
-
161
- spacing = width // 3
162
- self._emit(
163
- f"{' ':>11}{self.colors.dim}"
164
- f"{first_time:<{spacing}}{mid_time}{last_time:>{spacing}}"
165
- f"{self.colors.reset}"
166
- )
167
-
168
- def _build_grid(
169
- self,
170
- data: list[int],
171
- min_val: int,
172
- max_val: int,
173
- value_range: int,
174
- width: int,
175
- height: int,
176
- ) -> list[str]:
177
- """Build the ASCII grid for the graph.
178
-
179
- Args:
180
- data: List of values to plot
181
- min_val: Minimum value in data
182
- max_val: Maximum value in data
183
- value_range: max_val - min_val
184
- width: Graph width in characters
185
- height: Graph height in rows
186
-
187
- Returns:
188
- List of strings, one per row
189
- """
190
- n = len(data)
191
- if n == 0:
192
- return [" " * width for _ in range(height)]
193
-
194
- # Initialize grid with empty spaces
195
- grid = [[" " for _ in range(width)] for _ in range(height)]
196
-
197
- # Calculate y positions for each data point
198
- data_x = []
199
- data_y = []
200
- for i, val in enumerate(data):
201
- # Map index to x coordinate
202
- if n == 1:
203
- x = width // 2
204
- else:
205
- x = int((i) * (width - 1) / (n - 1))
206
- x = max(0, min(width - 1, x))
207
-
208
- # Map value to y coordinate (inverted: 0=top)
209
- if value_range == 0:
210
- y = height // 2
211
- else:
212
- y = int((max_val - val) * (height - 1) / value_range)
213
- y = max(0, min(height - 1, y))
214
-
215
- data_x.append(x)
216
- data_y.append(y)
217
-
218
- # Interpolate between points to fill every x position
219
- line_y = [-1.0] * width
220
- for i in range(len(data) - 1):
221
- x1, y1 = data_x[i], data_y[i]
222
- x2, y2 = data_x[i + 1], data_y[i + 1]
223
-
224
- # Ensure we don't go out of bounds
225
- for x in range(x1, min(x2 + 1, width)):
226
- if x2 == x1:
227
- y_interp = float(y1)
228
- else:
229
- # Linear interpolation
230
- t = (x - x1) / (x2 - x1)
231
- y_interp = y1 + t * (y2 - y1)
232
- line_y[x] = y_interp
233
-
234
- # Draw filled area and line
235
- for c in range(width):
236
- if line_y[c] >= 0:
237
- line_row = int(line_y[c] + 0.5) # Round to nearest integer
238
- line_row = max(0, min(height - 1, line_row))
239
-
240
- # Fill area below the line with gradient
241
- for r in range(line_row, height):
242
- if r == line_row:
243
- grid[r][c] = self.DOT
244
- elif r < line_row + 2:
245
- grid[r][c] = self.FILL_LIGHT
246
- else:
247
- grid[r][c] = self.FILL_DARK
248
-
249
- # Mark actual data points
250
- for i in range(len(data)):
251
- x = data_x[i]
252
- y = max(0, min(height - 1, int(data_y[i] + 0.5)))
253
- grid[y][x] = self.DOT
254
-
255
- # Convert grid to strings
256
- return ["".join(row) for row in grid]
257
-
258
- def render_summary(
259
- self,
260
- entries: list, # list[StateEntry]
261
- deltas: list[int],
262
- ) -> None:
263
- """Render summary statistics.
264
-
265
- Args:
266
- entries: List of StateEntry objects
267
- deltas: List of token deltas
268
- """
269
- if not entries:
270
- return
271
-
272
- first = entries[0]
273
- last = entries[-1]
274
- duration = last.timestamp - first.timestamp
275
-
276
- # Context window info - use current_used_tokens which represents actual context usage
277
- remaining_context = 0
278
- remaining_percentage = 0
279
- usage_percentage = 0
280
- if last.context_window_size > 0:
281
- # current_used_tokens = current_input_tokens + cache_creation + cache_read
282
- current_used = last.current_used_tokens
283
- remaining_context = max(0, last.context_window_size - current_used)
284
- remaining_percentage = remaining_context * 100 // last.context_window_size
285
- usage_percentage = 100 - remaining_percentage
286
-
287
- # Determine status based on context usage
288
- if usage_percentage < 40:
289
- status_color = self.colors.green
290
- status_text = "Smart Zone"
291
- status_hint = "You are in the smart zone"
292
- elif usage_percentage < 80:
293
- status_color = self.colors.yellow
294
- status_text = "Dumb Zone"
295
- status_hint = "You are in the dumb zone - Dex Horthy says so"
296
- else:
297
- status_color = self.colors.red
298
- status_text = "Wrap Up Zone"
299
- status_hint = "Better to wrap up and start a new session"
300
-
301
- self._emit()
302
- self._emit(f"{self.colors.bold}Session Summary{self.colors.reset}")
303
- line_width = self.dimensions.graph_width + 11
304
- self._emit(f"{self.colors.dim}{'-' * line_width}{self.colors.reset}")
305
-
306
- # Context remaining (before status)
307
- if last.context_window_size > 0:
308
- self._emit(
309
- f" {status_color}{'Context Remaining:':<20}{self.colors.reset} "
310
- f"{format_tokens(remaining_context, self.token_detail)}/{format_tokens(last.context_window_size, self.token_detail)} ({remaining_percentage}%)"
311
- )
312
-
313
- # Status indicator - highlighted
314
- if last.context_window_size > 0:
315
- self._emit(
316
- f" {status_color}{self.colors.bold}>>> {status_text.upper()} <<<{self.colors.reset} "
317
- f"{self.colors.dim}({status_hint}){self.colors.reset}"
318
- )
319
- self._emit()
320
-
321
- # Session details (ordered: Last Growth, I/O, Lines, Cost, Model, Duration)
322
- if deltas:
323
- current_growth = deltas[-1]
324
- self._emit(
325
- f" {self.colors.cyan}{'Last Growth:':<20}{self.colors.reset} "
326
- f"+{format_tokens(current_growth, self.token_detail)}"
327
- )
328
- self._emit(
329
- f" {self.colors.blue}{'Input Tokens:':<20}{self.colors.reset} "
330
- f"{format_tokens(last.current_input_tokens, self.token_detail)}"
331
- )
332
- self._emit(
333
- f" {self.colors.magenta}{'Output Tokens:':<20}{self.colors.reset} "
334
- f"{format_tokens(last.current_output_tokens, self.token_detail)}"
335
- )
336
- if last.lines_added > 0 or last.lines_removed > 0:
337
- self._emit(
338
- f" {self.colors.dim}{'Lines Changed:':<20}{self.colors.reset} "
339
- f"{self.colors.green}+{last.lines_added:,}{self.colors.reset} / "
340
- f"{self.colors.red}-{last.lines_removed:,}{self.colors.reset}"
341
- )
342
- if last.cost_usd > 0:
343
- self._emit(
344
- f" {self.colors.yellow}{'Total Cost:':<20}{self.colors.reset} ${last.cost_usd:.4f}"
345
- )
346
- if last.model_id:
347
- self._emit(f" {self.colors.dim}{'Model:':<20}{self.colors.reset} {last.model_id}")
348
- self._emit(
349
- f" {self.colors.cyan}{'Session Duration:':<20}{self.colors.reset} "
350
- f"{format_duration(duration)}"
351
- )
352
- self._emit()
353
-
354
- def render_footer(self, version: str = "1.6.1", commit_hash: str = "dev") -> None:
355
- """Render the footer with version info.
356
-
357
- Args:
358
- version: Package version
359
- commit_hash: Git commit hash
360
- """
361
- self._emit(
362
- f"{self.colors.dim}Powered by {self.colors.cyan}cc-context-stats"
363
- f"{self.colors.dim} v{version}-{commit_hash} - "
364
- f"https://github.com/luongnv89/cc-context-stats{self.colors.reset}"
365
- )
366
- self._emit()
@@ -1,92 +0,0 @@
1
- """Statistical calculations for token data."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
-
7
-
8
- @dataclass
9
- class Stats:
10
- """Statistical summary of a data series."""
11
-
12
- min_val: int
13
- max_val: int
14
- avg_val: int
15
- total: int
16
- count: int
17
-
18
-
19
- def calculate_stats(data: list[int]) -> Stats:
20
- """Calculate basic statistics for a list of integers.
21
-
22
- Args:
23
- data: List of integer values
24
-
25
- Returns:
26
- Stats object with min, max, avg, total, and count
27
- """
28
- if not data:
29
- return Stats(min_val=0, max_val=0, avg_val=0, total=0, count=0)
30
-
31
- min_val = min(data)
32
- max_val = max(data)
33
- total = sum(data)
34
- count = len(data)
35
- avg_val = total // count if count > 0 else 0
36
-
37
- return Stats(min_val=min_val, max_val=max_val, avg_val=avg_val, total=total, count=count)
38
-
39
-
40
- def detect_spike(deltas: list[int], context_window_size: int, window: int = 5) -> bool:
41
- """Check if the latest delta is a spike.
42
-
43
- A spike is defined as:
44
- - Latest delta > 15% of context window size, OR
45
- - Latest delta > 3x the rolling average of the last `window` deltas
46
-
47
- Args:
48
- deltas: List of token deltas
49
- context_window_size: Total context window size in tokens
50
- window: Number of recent deltas for rolling average (default: 5)
51
-
52
- Returns:
53
- True if the latest delta qualifies as a spike
54
- """
55
- if not deltas:
56
- return False
57
-
58
- latest = deltas[-1]
59
-
60
- # Check absolute threshold: > 15% of context window
61
- if context_window_size > 0 and latest > context_window_size * 0.15:
62
- return True
63
-
64
- # Check relative threshold: > 3x rolling average of previous deltas
65
- previous = deltas[-(window + 1):-1] if len(deltas) > window else deltas[:-1]
66
- if previous:
67
- avg = sum(previous) / len(previous)
68
- if avg > 0 and latest > avg * 3:
69
- return True
70
-
71
- return False
72
-
73
-
74
- def calculate_deltas(values: list[int]) -> list[int]:
75
- """Calculate deltas between consecutive values.
76
-
77
- Args:
78
- values: List of values (e.g., cumulative token counts)
79
-
80
- Returns:
81
- List of deltas (length = len(values) - 1)
82
- """
83
- if len(values) < 2:
84
- return []
85
-
86
- deltas = []
87
- for i in range(1, len(values)):
88
- delta = values[i] - values[i - 1]
89
- # Handle negative deltas (session reset) by showing 0
90
- deltas.append(max(0, delta))
91
-
92
- return deltas
@@ -1 +0,0 @@
1
- """UI components for context-stats display."""
@@ -1,93 +0,0 @@
1
- """Activity tier detection for token usage visualization."""
2
-
3
- from __future__ import annotations
4
-
5
- from enum import Enum
6
-
7
- from claude_statusline.core.state import StateEntry
8
- from claude_statusline.graphs.statistics import calculate_deltas, detect_spike
9
-
10
-
11
- class ActivityTier(Enum):
12
- """Token activity intensity tiers."""
13
-
14
- IDLE = "idle"
15
- LOW = "low"
16
- MEDIUM = "medium"
17
- HIGH = "high"
18
- SPIKE = "spike"
19
-
20
-
21
- # Tier labels for accessibility (understandable without color)
22
- TIER_LABELS: dict[ActivityTier, str] = {
23
- ActivityTier.IDLE: "Idle",
24
- ActivityTier.LOW: "Low activity",
25
- ActivityTier.MEDIUM: "Active",
26
- ActivityTier.HIGH: "High activity",
27
- ActivityTier.SPIKE: "Spike!",
28
- }
29
-
30
-
31
- def get_activity_tier(
32
- entries: list[StateEntry],
33
- context_window_size: int,
34
- ) -> ActivityTier:
35
- """Determine the current activity tier based on recent token deltas.
36
-
37
- Args:
38
- entries: List of StateEntry objects (chronological order)
39
- context_window_size: Total context window size in tokens
40
-
41
- Returns:
42
- The current ActivityTier
43
- """
44
- if len(entries) < 2:
45
- return ActivityTier.IDLE
46
-
47
- # Check if session is idle (>30s since last entry)
48
- import time
49
-
50
- now = int(time.time())
51
- last_timestamp = entries[-1].timestamp
52
- if now - last_timestamp > 30:
53
- return ActivityTier.IDLE
54
-
55
- # Calculate deltas from context usage
56
- context_used = [e.current_used_tokens for e in entries]
57
- deltas = calculate_deltas(context_used)
58
-
59
- if not deltas:
60
- return ActivityTier.IDLE
61
-
62
- latest_delta = deltas[-1]
63
-
64
- if context_window_size <= 0:
65
- return ActivityTier.LOW if latest_delta > 0 else ActivityTier.IDLE
66
-
67
- # Check for spike first (highest priority)
68
- if detect_spike(deltas, context_window_size):
69
- return ActivityTier.SPIKE
70
-
71
- # Calculate delta as percentage of context window
72
- delta_pct = (latest_delta / context_window_size) * 100
73
-
74
- if delta_pct > 5:
75
- return ActivityTier.HIGH
76
- elif delta_pct > 2:
77
- return ActivityTier.MEDIUM
78
- elif latest_delta > 0:
79
- return ActivityTier.LOW
80
- else:
81
- return ActivityTier.IDLE
82
-
83
-
84
- def get_tier_label(tier: ActivityTier) -> str:
85
- """Get an accessible text label for a tier.
86
-
87
- Args:
88
- tier: The activity tier
89
-
90
- Returns:
91
- Human-readable label string
92
- """
93
- return TIER_LABELS.get(tier, "")
@@ -1,62 +0,0 @@
1
- """Rotating waiting text for active sessions."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
-
7
- from claude_statusline.core.state import StateEntry
8
-
9
- WAITING_MESSAGES = [
10
- "Thinking...",
11
- "Cooking...",
12
- "Crunching tokens...",
13
- "Compiling plan...",
14
- "Running steps...",
15
- "Processing...",
16
- "Working on it...",
17
- "Analyzing...",
18
- ]
19
-
20
- # Static message for reduced-motion mode
21
- STATIC_MESSAGE = "Working..."
22
-
23
-
24
- def get_waiting_text(cycle_index: int, reduced_motion: bool = False) -> str:
25
- """Get the current waiting text based on the refresh cycle.
26
-
27
- Messages rotate every 2 cycles (approximately every 4 seconds at 2s refresh).
28
-
29
- Args:
30
- cycle_index: The current watch-mode refresh counter
31
- reduced_motion: If True, return a static message instead of rotating
32
-
33
- Returns:
34
- A waiting message string
35
- """
36
- if reduced_motion:
37
- return STATIC_MESSAGE
38
-
39
- # Rotate every 2 cycles to keep it readable
40
- message_index = (cycle_index // 2) % len(WAITING_MESSAGES)
41
- return WAITING_MESSAGES[message_index]
42
-
43
-
44
- def is_active(entries: list[StateEntry], timeout: int = 30) -> bool:
45
- """Determine if the session is currently active.
46
-
47
- A session is considered active if the most recent state entry
48
- was recorded within `timeout` seconds of the current time.
49
-
50
- Args:
51
- entries: List of StateEntry objects (chronological order)
52
- timeout: Seconds since last entry to consider session active (default: 30)
53
-
54
- Returns:
55
- True if the session appears to be actively running
56
- """
57
- if not entries:
58
- return False
59
-
60
- now = int(time.time())
61
- last_timestamp = entries[-1].timestamp
62
- return (now - last_timestamp) <= timeout