Created
February 19, 2026 11:01
-
-
Save damieng/2b8d1e41e8094d285b64504be0fc97a5 to your computer and use it in GitHub Desktop.
Node port of the bash status line for claude at https://github.com/ykdojo/claude-code-tips/blob/main/scripts/context-bar.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const { execSync } = require('child_process'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| let input = ''; | |
| process.stdin.on('data', chunk => input += chunk); | |
| process.stdin.on('end', () => { | |
| const data = JSON.parse(input); | |
| // Colors (256-color ANSI) | |
| const THEMES = { | |
| orange: '38;5;173', blue: '38;5;74', teal: '38;5;66', | |
| green: '38;5;71', lavender: '38;5;139', rose: '38;5;132', | |
| gold: '38;5;136', slate: '38;5;60', cyan: '38;5;37' | |
| }; | |
| const COLOR = 'blue'; | |
| const C_ACCENT = `\x1b[${THEMES[COLOR]}m`; | |
| const C_GRAY = '\x1b[38;5;245m'; | |
| const C_BAR_EMPTY = '\x1b[38;5;238m'; | |
| const C_RESET = '\x1b[0m'; | |
| // Model & directory | |
| const model = data.model?.display_name || data.model?.id || '?'; | |
| const dir = path.basename(data.cwd || data.workspace?.current_dir || ''); | |
| const maxContext = data.context_window?.context_window_size || 200000; | |
| const maxK = Math.floor(maxContext / 1000); | |
| const transcriptPath = data.transcript_path || ''; | |
| // Git info | |
| let gitInfo = ''; | |
| try { | |
| execSync('git rev-parse --git-dir', { stdio: 'ignore' }); | |
| const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim() || 'HEAD'; | |
| // Uncommitted changes | |
| const porcelain = execSync('git status --porcelain', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| let changes = ''; | |
| if (porcelain) { | |
| const lines = porcelain.split('\n').filter(Boolean); | |
| if (lines.length === 1) { | |
| const fname = lines[0].substring(3).trim().replace(/^"(.*)"$/, '$1'); | |
| changes = ` ${path.basename(fname)}`; | |
| } else { | |
| changes = ` ${lines.length} files`; | |
| } | |
| } | |
| // Sync status | |
| let sync = ''; | |
| try { | |
| const lr = execSync('git rev-list --left-right --count HEAD...@{upstream}', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| const [ahead, behind] = lr.split(/\s+/).map(Number); | |
| if (ahead === 0 && behind === 0) { | |
| // Fetch age | |
| let fetchAgo = ''; | |
| try { | |
| const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| const fetchHead = path.join(gitDir, 'FETCH_HEAD'); | |
| if (fs.existsSync(fetchHead)) { | |
| const ageSec = Math.floor((Date.now() - fs.statSync(fetchHead).mtimeMs) / 1000); | |
| if (ageSec < 60) fetchAgo = ' <1m ago'; | |
| else if (ageSec < 3600) fetchAgo = ` ${Math.floor(ageSec / 60)}m ago`; | |
| else if (ageSec < 86400) fetchAgo = ` ${Math.floor(ageSec / 3600)}h ago`; | |
| else fetchAgo = ` ${Math.floor(ageSec / 86400)}d ago`; | |
| } | |
| } catch {} | |
| sync = ` synced${fetchAgo}`; | |
| } else { | |
| const parts = []; | |
| if (ahead > 0) parts.push(`${ahead} ahead`); | |
| if (behind > 0) parts.push(`${behind} behind`); | |
| sync = ` ${parts.join(', ')}`; | |
| } | |
| } catch { | |
| sync = ' no upstream'; | |
| } | |
| gitInfo = ` | \u{1F500}${branch}${changes}${sync}`; | |
| } catch {} | |
| // Context tokens from transcript | |
| let contextLength = 20000; // baseline estimate | |
| let approximate = true; | |
| if (transcriptPath && fs.existsSync(transcriptPath)) { | |
| try { | |
| const raw = fs.readFileSync(transcriptPath, 'utf8'); | |
| const lines = raw.trim().split('\n').filter(Boolean); | |
| // Walk backwards to find last message with usage (not sidechain, not error) | |
| for (let i = lines.length - 1; i >= 0; i--) { | |
| try { | |
| const entry = JSON.parse(lines[i]); | |
| if (entry.message?.usage && !entry.isSidechain && !entry.isApiErrorMessage) { | |
| const u = entry.message.usage; | |
| const tokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0); | |
| if (tokens > 0) { | |
| contextLength = tokens; | |
| approximate = false; | |
| } | |
| break; | |
| } | |
| } catch {} | |
| } | |
| } catch {} | |
| } | |
| const pct = Math.floor((contextLength * 100) / maxContext); | |
| const pctStr = approximate ? `~${pct}%` : `${pct}%`; | |
| // Progress bar (10 cells, per-cell thresholds) | |
| let bar = ''; | |
| for (let i = 0; i < 10; i++) { | |
| const cellPct = pct - (i * 10); | |
| if (cellPct >= 8) bar += `${C_ACCENT}\u{2588}${C_RESET}`; | |
| else if (cellPct >= 3) bar += `${C_ACCENT}\u{2584}${C_RESET}`; | |
| else bar += `${C_BAR_EMPTY}\u{2591}${C_RESET}`; | |
| } | |
| // Line 1: model | dir | git | bar | |
| const line1 = `${C_ACCENT}${model}${C_GRAY} | \u{1F4C1}${dir}${gitInfo} | ${bar} ${pctStr} of ${maxK}k tokens${C_RESET}`; | |
| // Line 2: last user message from transcript | |
| let line2 = ''; | |
| if (transcriptPath && fs.existsSync(transcriptPath)) { | |
| try { | |
| const raw = fs.readFileSync(transcriptPath, 'utf8'); | |
| const lines = raw.trim().split('\n').filter(Boolean); | |
| for (let i = lines.length - 1; i >= 0; i--) { | |
| try { | |
| const entry = JSON.parse(lines[i]); | |
| if (entry.type === 'user') { | |
| let text = ''; | |
| const content = entry.message?.content; | |
| if (typeof content === 'string') { | |
| text = content; | |
| } else if (Array.isArray(content)) { | |
| text = content | |
| .filter(c => c.type === 'text') | |
| .map(c => c.text) | |
| .join(' '); | |
| } | |
| text = text.replace(/\s+/g, ' ').trim(); | |
| if (!text || text.startsWith('[Request interrupted') || text.startsWith('[Request cancelled')) { | |
| continue; | |
| } | |
| // Truncate to reasonable width | |
| const maxLen = (process.stdout.columns || 80) - 4; | |
| if (text.length > maxLen) { | |
| text = text.substring(0, maxLen - 3) + '...'; | |
| } | |
| line2 = `${C_GRAY}\u{1F4AC} ${text}${C_RESET}`; | |
| break; | |
| } | |
| } catch {} | |
| } | |
| } catch {} | |
| } | |
| console.log(line1); | |
| if (line2) console.log(line2); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment