mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-09-05 00:18:01 +00:00
Add git insertions / deletions count. Fixed context length calculation.
This commit is contained in:
+98
-32
@@ -44,9 +44,9 @@ async function readStdin(): Promise<string | null> {
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const chunks: string[] = [];
|
||||
|
||||
|
||||
try {
|
||||
// Use Node.js compatible approach
|
||||
if (typeof Bun !== 'undefined' && Bun.stdin) {
|
||||
@@ -80,6 +80,50 @@ function getGitBranch(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function getGitChanges(): { insertions: number; deletions: number } | null {
|
||||
try {
|
||||
let totalInsertions = 0;
|
||||
let totalDeletions = 0;
|
||||
|
||||
// Get unstaged changes
|
||||
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
// Get staged changes
|
||||
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
// Parse unstaged changes
|
||||
if (unstagedStat) {
|
||||
const insertMatch = unstagedStat.match(/(\d+) insertion/);
|
||||
const deleteMatch = unstagedStat.match(/(\d+) deletion/);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
// Parse staged changes
|
||||
if (stagedStat) {
|
||||
const insertMatch = stagedStat.match(/(\d+) insertion/);
|
||||
const deleteMatch = stagedStat.match(/(\d+) deletion/);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
// Return null if no changes at all (so the element doesn't appear)
|
||||
if (totalInsertions === 0 && totalDeletions === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { insertions: totalInsertions, deletions: totalDeletions };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -92,15 +136,17 @@ async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
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;
|
||||
|
||||
// Parse each line and sum up token usage
|
||||
let contextLength = 0;
|
||||
|
||||
// Parse each line and sum up token usage for totals
|
||||
let lastMessageWithUsage: TranscriptLine | null = null;
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data: TranscriptLine = JSON.parse(line);
|
||||
@@ -109,15 +155,25 @@ async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
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;
|
||||
|
||||
// Keep track of the last message with usage data
|
||||
lastMessageWithUsage = data;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate context length from the most recent message
|
||||
if (lastMessageWithUsage?.message?.usage) {
|
||||
const usage = lastMessageWithUsage.message.usage;
|
||||
contextLength = (usage.input_tokens || 0) +
|
||||
(usage.cache_read_input_tokens || 0) +
|
||||
(usage.cache_creation_input_tokens || 0);
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
const contextLength = inputTokens + outputTokens; // Current context size
|
||||
|
||||
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
@@ -129,24 +185,24 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
const terminalWidth = 80; // Always use 80 chars for Claude Code status line
|
||||
const elements: { content: string, type: string }[] = [];
|
||||
let hasFlexSeparator = false;
|
||||
|
||||
|
||||
// Get token metrics if needed
|
||||
const hasTokenItems = settings.items.some(item =>
|
||||
const hasTokenItems = settings.items.some(item =>
|
||||
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
|
||||
);
|
||||
|
||||
|
||||
let tokenMetrics: any = null;
|
||||
if (hasTokenItems && data.transcript_path) {
|
||||
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
||||
}
|
||||
|
||||
|
||||
// Helper function to format token counts
|
||||
const formatTokens = (count: number): string => {
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
|
||||
// Build elements based on configured items
|
||||
for (const item of settings.items) {
|
||||
switch (item.type) {
|
||||
@@ -156,7 +212,7 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
elements.push({ content: color(`Model: ${data.model.display_name}`), type: 'model' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'git-branch':
|
||||
const branch = getGitBranch();
|
||||
if (branch) {
|
||||
@@ -164,42 +220,52 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
elements.push({ content: color(`⎇ ${branch}`), type: 'git-branch' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'git-changes':
|
||||
const changes = getGitChanges();
|
||||
if (changes !== null) {
|
||||
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
// Compact format: (+42,-10)
|
||||
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
|
||||
elements.push({ content: color(changeStr), type: 'git-changes' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tokens-input':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
elements.push({ content: color(`In: ${formatTokens(tokenMetrics.inputTokens)}`), type: 'tokens-input' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'tokens-output':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'green'] || chalk.green;
|
||||
elements.push({ content: color(`Out: ${formatTokens(tokenMetrics.outputTokens)}`), type: 'tokens-output' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'tokens-cached':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
|
||||
elements.push({ content: color(`Cached: ${formatTokens(tokenMetrics.cachedTokens)}`), type: 'tokens-cached' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'tokens-total':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'white'] || chalk.white;
|
||||
elements.push({ content: color(`Total: ${formatTokens(tokenMetrics.totalTokens)}`), type: 'tokens-total' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'context-length':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push({ content: color(`Ctx: ${formatTokens(tokenMetrics.contextLength)}`), type: 'context-length' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'context-percentage':
|
||||
if (tokenMetrics) {
|
||||
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
|
||||
@@ -207,7 +273,7 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: 'context-percentage' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'separator':
|
||||
// Only add separator if there are already elements and the last one isn't a separator
|
||||
const lastElement = elements[elements.length - 1];
|
||||
@@ -216,24 +282,24 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
elements.push({ content: sepColor(' | '), type: 'separator' });
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'flex-separator':
|
||||
elements.push({ content: 'FLEX', type: 'flex-separator' });
|
||||
hasFlexSeparator = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
||||
|
||||
// Build the final status line
|
||||
let statusLine = '';
|
||||
|
||||
|
||||
if (hasFlexSeparator) {
|
||||
// Split elements by flex separators
|
||||
const parts: string[][] = [[]];
|
||||
let currentPart = 0;
|
||||
|
||||
|
||||
for (const elem of elements) {
|
||||
if (elem.type === 'flex-separator') {
|
||||
currentPart++;
|
||||
@@ -242,20 +308,20 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
parts[currentPart]!.push(elem.content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate total length of all non-flex content
|
||||
const partLengths = parts.map(part => {
|
||||
const joined = part.join('');
|
||||
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
});
|
||||
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
|
||||
|
||||
|
||||
// Calculate space to distribute among flex separators
|
||||
const flexCount = parts.length - 1; // Number of flex separators
|
||||
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
|
||||
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
|
||||
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
|
||||
|
||||
|
||||
// Build the status line with distributed spacing
|
||||
statusLine = '';
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
@@ -272,7 +338,7 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
} else {
|
||||
// No flex separator, just join all elements
|
||||
statusLine = elements.map(e => e.content).join('');
|
||||
|
||||
|
||||
// Pad to full width with spaces
|
||||
const contentLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
const remainingSpace = terminalWidth - contentLength;
|
||||
@@ -280,7 +346,7 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
statusLine = statusLine + ' '.repeat(remainingSpace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ensure we never exceed 80 chars
|
||||
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
if (plainLength > 80) {
|
||||
|
||||
+3
-1
@@ -8,7 +8,7 @@ const readFile = fs.promises?.readFile || promisify(fs.readFile);
|
||||
const writeFile = fs.promises?.writeFile || promisify(fs.writeFile);
|
||||
const mkdir = fs.promises?.mkdir || promisify(fs.mkdir);
|
||||
|
||||
export type StatusItemType = 'model' | 'git-branch' | 'separator' | 'flex-separator' |
|
||||
export type StatusItemType = 'model' | 'git-branch' | 'git-changes' | 'separator' | 'flex-separator' |
|
||||
'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage';
|
||||
|
||||
export interface StatusItem {
|
||||
@@ -34,6 +34,8 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
{ id: '1', type: 'model', color: 'cyan' },
|
||||
{ id: '2', type: 'separator' },
|
||||
{ id: '3', type: 'git-branch', color: 'magenta' },
|
||||
{ id: '4', type: 'separator' },
|
||||
{ id: '5', type: 'git-changes', color: 'yellow' },
|
||||
],
|
||||
colors: {
|
||||
model: 'cyan',
|
||||
|
||||
+9
-2
@@ -25,6 +25,10 @@ const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ items, terminalWi
|
||||
const branchColor = (chalk as any)[item.color || 'magenta'] || chalk.magenta;
|
||||
elements.push(branchColor('⎇ main'));
|
||||
break;
|
||||
case 'git-changes':
|
||||
const changesColor = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
elements.push(changesColor('(+42,-10)'));
|
||||
break;
|
||||
case 'tokens-input':
|
||||
const inputColor = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
elements.push(inputColor('In: 15.2k'));
|
||||
@@ -224,7 +228,7 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack }) =>
|
||||
setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1));
|
||||
} else if (key.leftArrow && items.length > 0) {
|
||||
// Toggle item type backwards
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator',
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'git-changes', 'separator', 'flex-separator',
|
||||
'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'];
|
||||
const currentItem = items[selectedIndex];
|
||||
if (currentItem) {
|
||||
@@ -240,7 +244,7 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack }) =>
|
||||
}
|
||||
} else if (key.rightArrow && items.length > 0) {
|
||||
// Toggle item type forwards
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator',
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'git-changes', 'separator', 'flex-separator',
|
||||
'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'];
|
||||
const currentItem = items[selectedIndex];
|
||||
if (currentItem) {
|
||||
@@ -293,6 +297,8 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack }) =>
|
||||
return chalk.cyan('Model');
|
||||
case 'git-branch':
|
||||
return chalk.magenta('Git Branch');
|
||||
case 'git-changes':
|
||||
return chalk.yellow('Git Changes');
|
||||
case 'separator':
|
||||
return chalk.dim('Separator |');
|
||||
case 'flex-separator':
|
||||
@@ -377,6 +383,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
|
||||
switch (item.type) {
|
||||
case 'model': return 'Model';
|
||||
case 'git-branch': return 'Git Branch';
|
||||
case 'git-changes': return 'Git Changes';
|
||||
case 'tokens-input': return 'Tokens Input';
|
||||
case 'tokens-output': return 'Tokens Output';
|
||||
case 'tokens-cached': return 'Tokens Cached';
|
||||
|
||||
Reference in New Issue
Block a user