mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-14 12:16:37 +00:00
Adding block timer widget, miscellaneous UI improvements
This commit is contained in:
1 parent
f19c24a39d
commit
d1ba0c0be5
33 files changed
+600
-163
No files matched your search
@@ -42,6 +42,16 @@
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
### v2.0.2 - Block Timer Widget
|
||||
|
||||

|
||||
|
||||
- **⏱️ Block Timer** - Track your progress through 5-hour Claude Code blocks
|
||||
- Displays time elapsed in current block as hours/minutes (e.g., "3hr 45m")
|
||||
- Progress bar mode shows visual completion percentage
|
||||
- Two progress bar styles: full width (32 chars) or compact (16 chars)
|
||||
- Automatically detects block boundaries from transcript timestamps
|
||||
|
||||
### v2.0.0 - Powerline Support & Enhanced Themes
|
||||
- **⚡ Powerline Mode** - Beautiful Powerline-style status lines with arrow separators and customizable caps
|
||||
- **🎨 Built-in Themes** - Multiple pre-configured themes that you can copy and customize
|
||||
@@ -55,7 +65,7 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **📊 Real-time Metrics** - Display model name, git branch, token usage, session duration, and more
|
||||
- **📊 Real-time Metrics** - Display model name, git branch, token usage, session duration, block timer, and more
|
||||
- **🎨 Fully Customizable** - Choose what to display and customize colors for each element
|
||||
- **⚡ Powerline Support** - Beautiful Powerline-style rendering with arrow separators, caps, and custom fonts
|
||||
- **📐 Multi-line Support** - Configure up to 3 independent status lines
|
||||
@@ -104,6 +114,7 @@ Once configured, ccstatusline automatically formats your Claude Code status line
|
||||
- **Git Branch** - Displays current git branch name
|
||||
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
|
||||
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
|
||||
- **Block Timer** - Shows time elapsed in current 5-hour block or progress bar
|
||||
- **Version** - Shows Claude Code version
|
||||
- **Output Style** - Shows the currently set output style in Claude Code
|
||||
- **Tokens Input** - Shows input tokens used
|
||||
@@ -161,11 +172,30 @@ Configure global formatting preferences that apply to all status items:
|
||||
|
||||
> ⚠️ **VSCode Users:** If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.
|
||||
|
||||
### ⏱️ Block Timer Widget
|
||||
|
||||
The Block Timer widget helps you track your progress through Claude Code's 5-hour conversation blocks:
|
||||
|
||||

|
||||
|
||||
**Display Modes:**
|
||||
- **Time Display** - Shows elapsed time as "3hr 45m" (default)
|
||||
- **Progress Bar** - Full width 32-character progress bar with percentage
|
||||
- **Progress Bar (Short)** - Compact 16-character progress bar with percentage
|
||||
|
||||
**Features:**
|
||||
- Automatically detects block boundaries from transcript timestamps
|
||||
- Floors block start time to the hour for consistent tracking
|
||||
- Shows "Block: 3hr 45m" in normal mode or just "3hr 45m" in raw value mode
|
||||
- Progress bars show completion percentage (e.g., "[████████████████████████░░░░░░░░] 73.9%")
|
||||
- Toggle between modes with the **(p)** key in the items editor
|
||||
|
||||
### 🔤 Raw Value Mode
|
||||
|
||||
Some items support "raw value" mode which displays just the value without a label:
|
||||
- Normal: `Model: Claude 3.5 Sonnet` → Raw: `Claude 3.5 Sonnet`
|
||||
- Normal: `Session: 2hr 15m` → Raw: `2hr 15m`
|
||||
- Normal: `Block: 3hr 45m` → Raw: `3hr 45m`
|
||||
- Normal: `Ctx: 18.6k` → Raw: `18.6k`
|
||||
|
||||
---
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^19.1.1",
|
||||
"react-devtools-core": "^6.1.5",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
},
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
@@ -37,6 +37,7 @@
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^19.1.1",
|
||||
"react-devtools-core": "^6.1.5",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.1"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
+12
-1
@@ -9,8 +9,11 @@ import {
|
||||
saveSettings
|
||||
} from './utils/config';
|
||||
import {
|
||||
getBlockMetrics,
|
||||
getSessionDuration,
|
||||
getTokenMetrics,
|
||||
getTokenMetrics
|
||||
} from './utils/jsonl';
|
||||
import {
|
||||
renderStatusLine,
|
||||
type RenderContext,
|
||||
type StatusJSON
|
||||
@@ -62,6 +65,9 @@ async function renderMultipleLines(data: StatusJSON) {
|
||||
// Check if session clock is needed
|
||||
const hasSessionClock = lines.some(line => line.some(item => item.type === 'session-clock'));
|
||||
|
||||
// Check if block timer is needed
|
||||
const hasBlockTimer = lines.some(line => line.some(item => item.type === 'block-timer'));
|
||||
|
||||
let tokenMetrics = null;
|
||||
if (hasTokenItems && data.transcript_path)
|
||||
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
||||
@@ -70,11 +76,16 @@ async function renderMultipleLines(data: StatusJSON) {
|
||||
if (hasSessionClock && data.transcript_path)
|
||||
sessionDuration = await getSessionDuration(data.transcript_path);
|
||||
|
||||
let blockMetrics = null;
|
||||
if (hasBlockTimer && data.transcript_path)
|
||||
blockMetrics = getBlockMetrics(data.transcript_path);
|
||||
|
||||
// Create render context
|
||||
const context: RenderContext = {
|
||||
data,
|
||||
tokenMetrics,
|
||||
sessionDuration,
|
||||
blockMetrics,
|
||||
isPreview: false
|
||||
};
|
||||
|
||||
|
||||
@@ -219,8 +219,10 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
|
||||
const newItems = widgets.map((widget) => {
|
||||
if (widget.id === selectedWidget.id) {
|
||||
// Remove color, backgroundColor, and bold properties
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { color, backgroundColor, bold, ...restWidget } = widget;
|
||||
void color; // Intentionally unused
|
||||
void backgroundColor; // Intentionally unused
|
||||
void bold; // Intentionally unused
|
||||
return restWidget;
|
||||
}
|
||||
return widget;
|
||||
@@ -430,8 +432,10 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
|
||||
// Clear all colors from all widgets
|
||||
const newItems = widgets.map((widget) => {
|
||||
// Remove color, backgroundColor, and bold properties
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { color, backgroundColor, bold, ...restWidget } = widget;
|
||||
void color; // Intentionally unused
|
||||
void backgroundColor; // Intentionally unused
|
||||
void bold; // Intentionally unused
|
||||
return restWidget;
|
||||
});
|
||||
onUpdate(newItems);
|
||||
|
||||
@@ -249,8 +249,8 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
}
|
||||
|
||||
if (nextMergeState === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { merge: _merge, ...rest } = currentWidget;
|
||||
const { merge, ...rest } = currentWidget;
|
||||
void merge; // Intentionally unused
|
||||
newWidgets[selectedIndex] = rest;
|
||||
} else {
|
||||
newWidgets[selectedIndex] = { ...currentWidget, merge: nextMergeState };
|
||||
@@ -270,11 +270,15 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
const matchedKeybind = customKeybinds.find(kb => kb.key === input);
|
||||
|
||||
if (matchedKeybind) {
|
||||
// Handle toggle actions directly
|
||||
if (matchedKeybind.action === 'toggle-preserve') {
|
||||
const newWidgets = [...widgets];
|
||||
newWidgets[selectedIndex] = { ...currentWidget, preserveColors: !currentWidget.preserveColors };
|
||||
onUpdate(newWidgets);
|
||||
// Check if widget handles the action directly
|
||||
if (widgetImpl.handleEditorAction) {
|
||||
// Let the widget handle the action directly
|
||||
const updatedWidget = widgetImpl.handleEditorAction(matchedKeybind.action, currentWidget);
|
||||
if (updatedWidget) {
|
||||
const newWidgets = [...widgets];
|
||||
newWidgets[selectedIndex] = updatedWidget;
|
||||
onUpdate(newWidgets);
|
||||
}
|
||||
} else if (widgetImpl.renderEditor) {
|
||||
// Set the action on the widget if it supports it
|
||||
if (widgetImpl.setEditorAction) {
|
||||
@@ -387,7 +391,9 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
)}
|
||||
</Box>
|
||||
{moveMode ? (
|
||||
<Text dimColor>↑↓ to move item, ESC or Enter to exit move mode</Text>
|
||||
<Box flexDirection='column' marginBottom={1}>
|
||||
<Text dimColor>↑↓ to move item, ESC or Enter to exit move mode</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection='column'>
|
||||
<Text dimColor>{helpText}</Text>
|
||||
@@ -404,17 +410,21 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
{widgets.length === 0 ? (
|
||||
<Text dimColor>No items. Press 'a' to add one.</Text>
|
||||
) : (
|
||||
widgets.map((widget, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const widgetImpl = widget.type !== 'separator' && widget.type !== 'flex-separator' ? getWidget(widget.type) : null;
|
||||
const { displayText, modifierText } = widgetImpl?.getEditorDisplay(widget) ?? { displayText: getWidgetDisplay(widget) };
|
||||
<>
|
||||
{widgets.map((widget, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const widgetImpl = widget.type !== 'separator' && widget.type !== 'flex-separator' ? getWidget(widget.type) : null;
|
||||
const { displayText, modifierText } = widgetImpl?.getEditorDisplay(widget) ?? { displayText: getWidgetDisplay(widget) };
|
||||
|
||||
return (
|
||||
<Box key={widget.id}>
|
||||
<Text>
|
||||
return (
|
||||
<Box key={widget.id} flexDirection='row' flexWrap='nowrap'>
|
||||
<Box width={3}>
|
||||
<Text color={isSelected ? (moveMode ? 'blue' : 'green') : undefined}>
|
||||
{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color={isSelected ? (moveMode ? 'blue' : 'green') : undefined}>
|
||||
{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}
|
||||
{` ${index + 1}. ${displayText || getWidgetDisplay(widget)}`}
|
||||
{`${index + 1}. ${displayText || getWidgetDisplay(widget)}`}
|
||||
</Text>
|
||||
{modifierText && (
|
||||
<Text dimColor>
|
||||
@@ -425,10 +435,31 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
{widget.rawValue && <Text dimColor> (raw value)</Text>}
|
||||
{widget.merge === true && <Text dimColor> (merged→)</Text>}
|
||||
{widget.merge === 'no-padding' && <Text dimColor> (merged-no-pad→)</Text>}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{/* Display description for selected widget */}
|
||||
{currentWidget && (
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text dimColor>
|
||||
{(() => {
|
||||
if (currentWidget.type === 'separator') {
|
||||
return 'A separator character between status line items';
|
||||
} else if (currentWidget.type === 'flex-separator') {
|
||||
return 'Expands to fill available terminal width';
|
||||
} else {
|
||||
try {
|
||||
const widgetImpl = getWidget(currentWidget.type);
|
||||
return widgetImpl.getDescription();
|
||||
} catch {
|
||||
return 'Widget description not available';
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { BlockInfo } from '../utils/jsonl';
|
||||
|
||||
import type { StatusJSON } from './StatusJSON';
|
||||
import type { TokenMetrics } from './TokenMetrics';
|
||||
|
||||
@@ -5,6 +7,7 @@ export interface RenderContext {
|
||||
data?: StatusJSON;
|
||||
tokenMetrics?: TokenMetrics | null;
|
||||
sessionDuration?: string | null;
|
||||
blockMetrics?: BlockInfo | null;
|
||||
terminalWidth?: number | null;
|
||||
isPreview?: boolean;
|
||||
lineIndex?: number; // Index of the current line being rendered (for theme cycling)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PowerlineConfigSchema } from './PowerlineConfig';
|
||||
import { WidgetItemSchema } from './Widget';
|
||||
|
||||
// Current version - bump this when making breaking changes to the schema
|
||||
export const CURRENT_VERSION = 2;
|
||||
export const CURRENT_VERSION = 3;
|
||||
|
||||
// Schema for v1 settings (before version field was added)
|
||||
export const SettingsSchema_v1 = z.object({
|
||||
|
||||
+4
-1
@@ -17,7 +17,8 @@ export const WidgetItemSchema = z.object({
|
||||
maxWidth: z.number().optional(),
|
||||
preserveColors: z.boolean().optional(),
|
||||
timeout: z.number().optional(),
|
||||
merge: z.union([z.boolean(), z.literal('no-padding')]).optional()
|
||||
merge: z.union([z.boolean(), z.literal('no-padding')]).optional(),
|
||||
metadata: z.record(z.string(), z.string()).optional()
|
||||
});
|
||||
|
||||
// Inferred types from Zod schemas
|
||||
@@ -31,6 +32,7 @@ export interface WidgetEditorDisplay {
|
||||
|
||||
export interface Widget {
|
||||
getDefaultColor(): string;
|
||||
getDescription(): string;
|
||||
getDisplayName(): string;
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null;
|
||||
@@ -39,6 +41,7 @@ export interface Widget {
|
||||
supportsRawValue(): boolean;
|
||||
supportsColors(item: WidgetItem): boolean;
|
||||
setEditorAction?(action: string): void;
|
||||
handleEditorAction?(action: string, item: WidgetItem): WidgetItem | null;
|
||||
}
|
||||
|
||||
export interface WidgetEditorProps {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import * as fs from 'fs';
|
||||
import path from 'node:path';
|
||||
import { globSync } from 'tinyglobby';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import type {
|
||||
TokenMetrics,
|
||||
TranscriptLine
|
||||
} from '../types';
|
||||
|
||||
// Ensure fs.promises compatibility for older Node versions
|
||||
const readFile = promisify(fs.readFile);
|
||||
const readFileSync = fs.readFileSync;
|
||||
const statSync = fs.statSync;
|
||||
|
||||
export interface BlockInfo {
|
||||
startTime: Date;
|
||||
lastActivity: Date;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export async function getSessionDuration(transcriptPath: string): Promise<string | null> {
|
||||
try {
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter((line: string) => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let firstTimestamp: Date | null = null;
|
||||
let lastTimestamp: Date | null = null;
|
||||
|
||||
// Find first valid timestamp
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
firstTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
// Find last valid timestamp (iterate backwards)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const data = JSON.parse(lines[i] ?? '') as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
lastTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTimestamp || !lastTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate duration in milliseconds
|
||||
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
|
||||
|
||||
// Convert to minutes
|
||||
const totalMinutes = Math.floor(durationMs / (1000 * 60));
|
||||
|
||||
if (totalMinutes < 1) {
|
||||
return '<1m';
|
||||
}
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes}m`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours}hr`;
|
||||
} else {
|
||||
return `${hours}hr ${minutes}m`;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenMetrics(transcriptPath: string): Promise<TokenMetrics> {
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let cachedTokens = 0;
|
||||
let contextLength = 0;
|
||||
|
||||
// Parse each line and sum up token usage for totals
|
||||
let mostRecentMainChainEntry: TranscriptLine | null = null;
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as TranscriptLine;
|
||||
if (data.message?.usage) {
|
||||
inputTokens += data.message.usage.input_tokens || 0;
|
||||
outputTokens += data.message.usage.output_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_read_input_tokens ?? 0;
|
||||
cachedTokens += data.message.usage.cache_creation_input_tokens ?? 0;
|
||||
|
||||
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
|
||||
if (data.isSidechain !== true && data.timestamp) {
|
||||
const entryTime = new Date(data.timestamp);
|
||||
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
||||
mostRecentTimestamp = entryTime;
|
||||
mostRecentMainChainEntry = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate context length from the most recent main chain message
|
||||
if (mostRecentMainChainEntry?.message?.usage) {
|
||||
const usage = mostRecentMainChainEntry.message.usage;
|
||||
contextLength = (usage.input_tokens || 0)
|
||||
+ (usage.cache_read_input_tokens ?? 0)
|
||||
+ (usage.cache_creation_input_tokens ?? 0);
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets block metrics for the current 5-hour block from JSONL files
|
||||
*/
|
||||
export function getBlockMetrics(transcriptPath: string | undefined): BlockInfo | null {
|
||||
if (!transcriptPath || typeof transcriptPath !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Walk up the directory tree to find .claude folder
|
||||
let currentPath = path.dirname(transcriptPath);
|
||||
let claudePath: string | null = null;
|
||||
|
||||
while (currentPath && currentPath !== path.dirname(currentPath)) {
|
||||
const baseName = path.basename(currentPath);
|
||||
if (baseName === '.claude') {
|
||||
claudePath = currentPath;
|
||||
break;
|
||||
}
|
||||
currentPath = path.dirname(currentPath);
|
||||
}
|
||||
|
||||
if (!claudePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return findMostRecentBlockStartTime(claudePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently finds the most recent 5-hour block start time from JSONL files
|
||||
* Uses file modification times as hints to avoid unnecessary reads
|
||||
*/
|
||||
function findMostRecentBlockStartTime(
|
||||
rootDir: string,
|
||||
sessionDurationHours = 5
|
||||
): BlockInfo | null {
|
||||
const sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
|
||||
// Step 1: Find all JSONL files with their modification times
|
||||
const pattern = path.join(rootDir, 'projects', '**', '*.jsonl').replace(/\\/g, '/');
|
||||
const files = globSync([pattern]);
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Get file stats and sort by modification time (most recent first)
|
||||
const filesWithStats = files.map((file) => {
|
||||
const stats = statSync(file);
|
||||
return { file, mtime: stats.mtime };
|
||||
});
|
||||
|
||||
filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
// Step 3: Collect recent timestamps to properly identify blocks
|
||||
// We need to look back far enough to find the start of the current/recent block
|
||||
const timestamps: Date[] = [];
|
||||
// Look back 20 hours to find the current session
|
||||
const cutoffTime = new Date(now.getTime() - 20 * 60 * 60 * 1000);
|
||||
|
||||
for (const { file, mtime } of filesWithStats) {
|
||||
// Skip files that are too old (optimization)
|
||||
if (mtime.getTime() < cutoffTime.getTime()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get all timestamps from this file (not just the latest)
|
||||
const fileTimestamps = getAllTimestampsFromFile(file);
|
||||
timestamps.push(...fileTimestamps);
|
||||
}
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 4: Sort timestamps and identify the most recent block
|
||||
timestamps.sort((a, b) => b.getTime() - a.getTime()); // Most recent first
|
||||
|
||||
// Find the most recent timestamp
|
||||
const mostRecentTimestamp = timestamps[0];
|
||||
if (!mostRecentTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now work backwards to find the start of this block
|
||||
let blockStartTimestamp: Date = mostRecentTimestamp;
|
||||
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
const currentTimestamp = timestamps[i];
|
||||
const previousTimestamp = timestamps[i - 1];
|
||||
|
||||
if (!currentTimestamp || !previousTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if there's a gap larger than session duration
|
||||
const gap = previousTimestamp.getTime() - currentTimestamp.getTime();
|
||||
|
||||
if (gap > sessionDurationMs) {
|
||||
// We found a gap - the current block starts with previousTimestamp
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we're still within the session duration from the most recent
|
||||
const timeFromMostRecent = mostRecentTimestamp.getTime() - currentTimestamp.getTime();
|
||||
|
||||
if (timeFromMostRecent < sessionDurationMs) {
|
||||
// This timestamp is still part of the current block
|
||||
blockStartTimestamp = currentTimestamp;
|
||||
} else {
|
||||
// We've gone too far back
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Floor the block start to the hour
|
||||
const blockStart = floorToHour(blockStartTimestamp);
|
||||
const timeSinceActivity = now.getTime() - mostRecentTimestamp.getTime();
|
||||
const blockEnd = new Date(blockStart.getTime() + sessionDurationMs);
|
||||
|
||||
// Check if block is still active
|
||||
const isActive = timeSinceActivity < sessionDurationMs && now < blockEnd;
|
||||
|
||||
return {
|
||||
startTime: blockStart,
|
||||
lastActivity: mostRecentTimestamp,
|
||||
isActive
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all timestamps from a JSONL file
|
||||
*/
|
||||
function getAllTimestampsFromFile(filePath: string): Date[] {
|
||||
const timestamps: Date[] = [];
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.length > 0);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line) as { timestamp?: string };
|
||||
if (json.timestamp && typeof json.timestamp === 'string') {
|
||||
const date = new Date(json.timestamp);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
timestamps.push(date);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floors a timestamp to the beginning of the hour (matching existing logic)
|
||||
*/
|
||||
function floorToHour(timestamp: Date): Date {
|
||||
const floored = new Date(timestamp);
|
||||
floored.setUTCMinutes(0, 0, 0);
|
||||
return floored;
|
||||
}
|
||||
@@ -89,6 +89,26 @@ export const migrations: Migration[] = [
|
||||
remaining: 12
|
||||
};
|
||||
|
||||
return migrated;
|
||||
}
|
||||
},
|
||||
{
|
||||
fromVersion: 2,
|
||||
toVersion: 3,
|
||||
description: 'Migrate from v2 to v3',
|
||||
migrate: (data) => {
|
||||
// Copy all existing data to v3
|
||||
const migrated: Record<string, unknown> = { ...data };
|
||||
|
||||
// Update version to 3
|
||||
migrated.version = 3;
|
||||
|
||||
// Add update message for v3 migration
|
||||
migrated.updatemessage = {
|
||||
message: 'ccstatusline updated to v2.0.2, 5hr block timer widget added',
|
||||
remaining: 12
|
||||
};
|
||||
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
// ANSI escape sequence for stripping color codes
|
||||
const ANSI_REGEX = new RegExp(`\\x1b\\[[0-9;]*m`, 'g');
|
||||
|
||||
import type {
|
||||
RenderContext,
|
||||
TokenMetrics,
|
||||
TranscriptLine,
|
||||
WidgetItem
|
||||
} from '../types';
|
||||
import { getColorLevelString } from '../types/ColorLevel';
|
||||
@@ -28,9 +24,6 @@ export type { StatusJSON } from '../types/StatusJSON';
|
||||
export type { TokenMetrics, TokenUsage, TranscriptLine } from '../types/TokenMetrics';
|
||||
export type { RenderContext } from '../types/RenderContext';
|
||||
|
||||
// Ensure fs.promises compatibility for older Node versions
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
// Color functions moved to colors.ts
|
||||
// Re-exported for backward compatibility
|
||||
export { applyColors } from './colors';
|
||||
@@ -47,135 +40,6 @@ export function formatTokens(count: number): string {
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
export async function getSessionDuration(transcriptPath: string): Promise<string | null> {
|
||||
try {
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter((line: string) => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let firstTimestamp: Date | null = null;
|
||||
let lastTimestamp: Date | null = null;
|
||||
|
||||
// Find first valid timestamp
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
firstTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
// Find last valid timestamp (iterate backwards)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const data = JSON.parse(lines[i] ?? '') as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
lastTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTimestamp || !lastTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate duration in milliseconds
|
||||
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
|
||||
|
||||
// Convert to minutes
|
||||
const totalMinutes = Math.floor(durationMs / (1000 * 60));
|
||||
|
||||
if (totalMinutes < 1) {
|
||||
return '<1m';
|
||||
}
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes}m`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours}hr`;
|
||||
} else {
|
||||
return `${hours}hr ${minutes}m`;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenMetrics(transcriptPath: string): Promise<TokenMetrics> {
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let cachedTokens = 0;
|
||||
let contextLength = 0;
|
||||
|
||||
// Parse each line and sum up token usage for totals
|
||||
let mostRecentMainChainEntry: TranscriptLine | null = null;
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as TranscriptLine;
|
||||
if (data.message?.usage) {
|
||||
inputTokens += data.message.usage.input_tokens || 0;
|
||||
outputTokens += data.message.usage.output_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_read_input_tokens ?? 0;
|
||||
cachedTokens += data.message.usage.cache_creation_input_tokens ?? 0;
|
||||
|
||||
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
|
||||
if (data.isSidechain !== true && data.timestamp) {
|
||||
const entryTime = new Date(data.timestamp);
|
||||
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
||||
mostRecentTimestamp = entryTime;
|
||||
mostRecentMainChainEntry = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate context length from the most recent main chain message
|
||||
if (mostRecentMainChainEntry?.message?.usage) {
|
||||
const usage = mostRecentMainChainEntry.message.usage;
|
||||
contextLength = (usage.input_tokens || 0)
|
||||
+ (usage.cache_read_input_tokens ?? 0)
|
||||
+ (usage.cache_creation_input_tokens ?? 0);
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function renderPowerlineStatusLine(
|
||||
widgets: WidgetItem[],
|
||||
settings: Settings,
|
||||
|
||||
@@ -19,6 +19,7 @@ const widgetRegistry = new Map<WidgetItemType, Widget>([
|
||||
['context-percentage', new widgets.ContextPercentageWidget()],
|
||||
['context-percentage-usable', new widgets.ContextPercentageUsableWidget()],
|
||||
['session-clock', new widgets.SessionClockWidget()],
|
||||
['block-timer', new widgets.BlockTimerWidget()],
|
||||
['terminal-width', new widgets.TerminalWidthWidget()],
|
||||
['version', new widgets.VersionWidget()],
|
||||
['custom-text', new widgets.CustomTextWidget()],
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
|
||||
type DisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
export class BlockTimerWidget implements Widget {
|
||||
getDefaultColor(): string { return 'yellow'; }
|
||||
getDescription(): string { return 'Shows elapsed time since beginning of current 5hr block'; }
|
||||
getDisplayName(): string { return 'Block Timer'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const mode = item.metadata?.display ?? 'time';
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
const currentMode = (item.metadata?.display ?? 'time') as DisplayMode;
|
||||
let nextMode: DisplayMode;
|
||||
|
||||
if (currentMode === 'time') {
|
||||
nextMode = 'progress';
|
||||
} else if (currentMode === 'progress') {
|
||||
nextMode = 'progress-short';
|
||||
} else {
|
||||
nextMode = 'time';
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
display: nextMode
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = (item.metadata?.display ?? 'time') as DisplayMode;
|
||||
|
||||
if (context.isPreview) {
|
||||
if (displayMode === 'progress') {
|
||||
return 'Block [██████████████████████░░░░░░░░] 73.9%';
|
||||
} else if (displayMode === 'progress-short') {
|
||||
return 'Block [███████░░░░░░░░] 73.9%';
|
||||
}
|
||||
return item.rawValue ? '3hr 45m' : 'Block: 3hr 45m';
|
||||
}
|
||||
|
||||
// Check if we have block metrics in context
|
||||
const blockInfo = context.blockMetrics;
|
||||
if (!blockInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Calculate elapsed time and progress
|
||||
const now = new Date();
|
||||
const elapsedMs = now.getTime() - blockInfo.startTime.getTime();
|
||||
const sessionDurationMs = 5 * 60 * 60 * 1000; // 5 hours
|
||||
const progress = Math.min(elapsedMs / sessionDurationMs, 1.0);
|
||||
const percentage = (progress * 100).toFixed(1);
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
const filledWidth = Math.floor(progress * barWidth);
|
||||
const emptyWidth = barWidth - filledWidth;
|
||||
const progressBar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
|
||||
|
||||
if (item.rawValue) {
|
||||
return `[${progressBar}] ${percentage}%`;
|
||||
} else {
|
||||
return `Block [${progressBar}] ${percentage}%`;
|
||||
}
|
||||
} else {
|
||||
// Time display mode
|
||||
const elapsedHours = Math.floor(elapsedMs / (1000 * 60 * 60));
|
||||
const elapsedMinutes = Math.floor((elapsedMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
let timeString: string;
|
||||
if (elapsedMinutes === 0) {
|
||||
timeString = `${elapsedHours}hr`;
|
||||
} else {
|
||||
timeString = `${elapsedHours}hr ${elapsedMinutes}m`;
|
||||
}
|
||||
|
||||
return item.rawValue ? timeString : `Block: ${timeString}`;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { formatTokens } from '../utils/renderer';
|
||||
|
||||
export class ContextLengthWidget implements Widget {
|
||||
getDefaultColor(): string { return 'brightBlack'; }
|
||||
getDescription(): string { return 'Shows the current context window size in tokens'; }
|
||||
getDisplayName(): string { return 'Context Length'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class ContextPercentageWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows percentage of context window used (of 200k tokens)'; }
|
||||
getDisplayName(): string { return 'Context %'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class ContextPercentageUsableWidget implements Widget {
|
||||
getDefaultColor(): string { return 'green'; }
|
||||
getDescription(): string { return 'Shows percentage of usable context window used (of 160k tokens before auto-compact)'; }
|
||||
getDisplayName(): string { return 'Context % (usable)'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -20,6 +20,7 @@ export class CustomCommandWidget implements Widget {
|
||||
private currentAction: string = 'edit-command';
|
||||
|
||||
getDefaultColor(): string { return 'white'; }
|
||||
getDescription(): string { return 'Executes a custom shell command and displays output'; }
|
||||
getDisplayName(): string { return 'Custom Command'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
@@ -49,6 +50,13 @@ export class CustomCommandWidget implements Widget {
|
||||
this.currentAction = action;
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-preserve') {
|
||||
return { ...item, preserveColors: !item.preserveColors };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
|
||||
export class CustomTextWidget implements Widget {
|
||||
getDefaultColor(): string { return 'white'; }
|
||||
getDescription(): string { return 'Displays user-defined custom text'; }
|
||||
getDisplayName(): string { return 'Custom Text'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
|
||||
export class GitBranchWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
getDescription(): string { return 'Shows the current git branch name'; }
|
||||
getDisplayName(): string { return 'Git Branch'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
|
||||
export class GitChangesWidget implements Widget {
|
||||
getDefaultColor(): string { return 'yellow'; }
|
||||
getDescription(): string { return 'Shows git changes count (+insertions, -deletions)'; }
|
||||
getDisplayName(): string { return 'Git Changes'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class ModelWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Displays the Claude model name (e.g., Claude 3.5 Sonnet)'; }
|
||||
getDisplayName(): string { return 'Model'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class OutputStyleWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows the current Claude Code output style'; }
|
||||
getDisplayName(): string { return 'Output Style'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class SessionClockWidget implements Widget {
|
||||
getDefaultColor(): string { return 'yellow'; }
|
||||
getDescription(): string { return 'Shows elapsed time since current session started'; }
|
||||
getDisplayName(): string { return 'Session Clock'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTerminalWidth } from '../utils/renderer';
|
||||
|
||||
export class TerminalWidthWidget implements Widget {
|
||||
getDefaultColor(): string { return 'gray'; }
|
||||
getDescription(): string { return 'Shows current terminal width in columns'; }
|
||||
getDisplayName(): string { return 'Terminal Width'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatTokens } from '../utils/renderer';
|
||||
|
||||
export class TokensCachedWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows cached token count for the current session'; }
|
||||
getDisplayName(): string { return 'Tokens Cached'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatTokens } from '../utils/renderer';
|
||||
|
||||
export class TokensInputWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows input token count for the current session'; }
|
||||
getDisplayName(): string { return 'Tokens Input'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatTokens } from '../utils/renderer';
|
||||
|
||||
export class TokensOutputWidget implements Widget {
|
||||
getDefaultColor(): string { return 'white'; }
|
||||
getDescription(): string { return 'Shows output token count for the current session'; }
|
||||
getDisplayName(): string { return 'Tokens Output'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatTokens } from '../utils/renderer';
|
||||
|
||||
export class TokensTotalWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows total token count (input + output + cache) for the current session'; }
|
||||
getDisplayName(): string { return 'Tokens Total'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
|
||||
export class VersionWidget implements Widget {
|
||||
getDefaultColor(): string { return 'gray'; }
|
||||
getDescription(): string { return 'Shows Claude Code CLI version number'; }
|
||||
getDisplayName(): string { return 'Version'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
|
||||
@@ -13,4 +13,5 @@ export { SessionClockWidget } from './SessionClock';
|
||||
export { TerminalWidthWidget } from './TerminalWidth';
|
||||
export { VersionWidget } from './Version';
|
||||
export { CustomTextWidget } from './CustomText';
|
||||
export { CustomCommandWidget } from './CustomCommand';
|
||||
export { CustomCommandWidget } from './CustomCommand';
|
||||
export { BlockTimerWidget } from './BlockTimer';
|
||||
Reference in new issue
Block a user