mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-14 06:17:28 +00:00
Change default flex mode compaction to 60%, make sensible default status line when no config present
This commit is contained in:
1 parent
d9dd43a15c
commit
7d7dd2084b
4 files changed
+81
-56
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.12",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
|
||||
+28
-28
@@ -79,7 +79,7 @@ function getTerminalWidth(): number | null {
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
shell: '/bin/sh'
|
||||
}).trim();
|
||||
|
||||
|
||||
// Check if we got a valid tty (not ?? which means no tty)
|
||||
if (tty && tty !== '??' && tty !== '?') {
|
||||
// Now get the terminal size
|
||||
@@ -91,7 +91,7 @@ function getTerminalWidth(): number | null {
|
||||
shell: '/bin/sh'
|
||||
}
|
||||
).trim();
|
||||
|
||||
|
||||
const parsed = parseInt(width, 10);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
@@ -100,14 +100,14 @@ function getTerminalWidth(): number | null {
|
||||
} catch {
|
||||
// Command failed, width detection not available
|
||||
}
|
||||
|
||||
|
||||
// Fallback: try tput cols which might work in some environments
|
||||
try {
|
||||
const width = execSync('tput cols 2>/dev/null', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
|
||||
const parsed = parseInt(width, 10);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
@@ -115,7 +115,7 @@ function getTerminalWidth(): number | null {
|
||||
} catch {
|
||||
// tput also failed
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ async function getSessionDuration(transcriptPath: string): Promise<string | null
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.trim());
|
||||
|
||||
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -219,10 +219,10 @@ async function getSessionDuration(transcriptPath: string): Promise<string | 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';
|
||||
}
|
||||
@@ -266,7 +266,7 @@ async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
// 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: TranscriptLine = JSON.parse(line);
|
||||
@@ -275,7 +275,7 @@ 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;
|
||||
|
||||
|
||||
// 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);
|
||||
@@ -293,8 +293,8 @@ async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
// 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) +
|
||||
contextLength = (usage.input_tokens || 0) +
|
||||
(usage.cache_read_input_tokens || 0) +
|
||||
(usage.cache_creation_input_tokens || 0);
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
|
||||
let terminalWidth: number | null = null;
|
||||
if (detectedWidth) {
|
||||
const flexMode = settings.flexMode || 'full-minus-40';
|
||||
|
||||
|
||||
if (flexMode === 'full') {
|
||||
// Use full width minus 4 for terminal padding
|
||||
terminalWidth = detectedWidth - 4;
|
||||
@@ -321,10 +321,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else if (flexMode === 'full-until-compact') {
|
||||
// Check context percentage to decide
|
||||
const threshold = settings.compactThreshold || 75;
|
||||
const contextPercentage = tokenMetrics ?
|
||||
const threshold = settings.compactThreshold || 60;
|
||||
const contextPercentage = tokenMetrics ?
|
||||
Math.min(100, (tokenMetrics.contextLength / 200000) * 100) : 0;
|
||||
|
||||
|
||||
if (contextPercentage >= threshold) {
|
||||
// Context is high, leave space for auto-compact
|
||||
terminalWidth = detectedWidth - 40;
|
||||
@@ -544,7 +544,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
|
||||
if (detectedWidth && detectedWidth > 0) {
|
||||
// Remove ANSI escape codes to get actual length
|
||||
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
|
||||
|
||||
if (plainLength > detectedWidth) {
|
||||
// Need to truncate - preserve ANSI codes while truncating
|
||||
let truncated = '';
|
||||
@@ -552,10 +552,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
|
||||
let inAnsiCode = false;
|
||||
let ansiBuffer = '';
|
||||
const targetLength = detectedWidth - 7; // Truncate to width-7 for proper fit
|
||||
|
||||
|
||||
for (let i = 0; i < statusLine.length; i++) {
|
||||
const char = statusLine[i];
|
||||
|
||||
|
||||
if (char === '\x1b') {
|
||||
inAnsiCode = true;
|
||||
ansiBuffer = char;
|
||||
@@ -575,17 +575,17 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
statusLine = truncated + '...';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return statusLine;
|
||||
}
|
||||
|
||||
async function renderStatusLine(data: StatusJSON) {
|
||||
const settings = await loadSettings();
|
||||
|
||||
|
||||
// Get all lines to render (support both old items format and new lines format)
|
||||
let lines: StatusItem[][] = [];
|
||||
if (settings.lines) {
|
||||
@@ -596,29 +596,29 @@ async function renderStatusLine(data: StatusJSON) {
|
||||
} else {
|
||||
lines = [[]];
|
||||
}
|
||||
|
||||
|
||||
// Get token metrics if needed (check all lines)
|
||||
const hasTokenItems = lines.some(line =>
|
||||
const hasTokenItems = lines.some(line =>
|
||||
line.some(item =>
|
||||
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// Check if session clock is needed
|
||||
const hasSessionClock = lines.some(line =>
|
||||
line.some(item => item.type === 'session-clock')
|
||||
);
|
||||
|
||||
|
||||
let tokenMetrics: any = null;
|
||||
if (hasTokenItems && data.transcript_path) {
|
||||
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
||||
}
|
||||
|
||||
|
||||
let sessionDuration: string | null = null;
|
||||
if (hasSessionClock && data.transcript_path) {
|
||||
sessionDuration = await getSessionDuration(data.transcript_path);
|
||||
}
|
||||
|
||||
|
||||
// Render each line
|
||||
for (const lineItems of lines) {
|
||||
if (lineItems.length > 0) {
|
||||
|
||||
+46
-21
@@ -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' | 'git-changes' | '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' | 'terminal-width' | 'session-clock' | 'version' | 'custom-text';
|
||||
|
||||
export interface StatusItem {
|
||||
@@ -40,13 +40,38 @@ const SETTINGS_PATH = path.join(CONFIG_DIR, 'settings.json');
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
lines: [
|
||||
[
|
||||
{ id: '1', type: 'model', color: 'cyan' },
|
||||
{ id: '2', type: 'separator' },
|
||||
{ id: '3', type: 'terminal-width', color: 'dim' },
|
||||
{ id: '4', type: 'separator' },
|
||||
{ id: '5', type: 'git-branch', color: 'magenta' },
|
||||
{ id: '6', type: 'separator' },
|
||||
{ id: '7', type: 'git-changes', color: 'yellow' },
|
||||
{
|
||||
"id": "1",
|
||||
"type": "model",
|
||||
"color": "cyan"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"type": "context-length",
|
||||
"color": "dim"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"type": "git-branch",
|
||||
"color": "magenta"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"type": "git-changes",
|
||||
"color": "yellow"
|
||||
}
|
||||
]
|
||||
],
|
||||
colors: {
|
||||
@@ -55,7 +80,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
separator: 'dim',
|
||||
},
|
||||
flexMode: 'full-minus-40',
|
||||
compactThreshold: 75,
|
||||
compactThreshold: 60,
|
||||
};
|
||||
|
||||
export async function loadSettings(): Promise<Settings> {
|
||||
@@ -64,10 +89,10 @@ export async function loadSettings(): Promise<Settings> {
|
||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
|
||||
const content = await readFile(SETTINGS_PATH, 'utf-8');
|
||||
let loaded: any;
|
||||
|
||||
|
||||
try {
|
||||
loaded = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
@@ -75,18 +100,18 @@ export async function loadSettings(): Promise<Settings> {
|
||||
console.error('Failed to parse settings.json, using defaults');
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
|
||||
// Migrate from old format with elements/layout
|
||||
if (loaded.elements || loaded.layout) {
|
||||
return migrateOldSettings(loaded);
|
||||
}
|
||||
|
||||
|
||||
// Migrate from single items array to lines array
|
||||
if (loaded.items && !loaded.lines) {
|
||||
loaded.lines = [loaded.items];
|
||||
delete loaded.items;
|
||||
}
|
||||
|
||||
|
||||
// Ensure lines is an array and limit to 3 lines
|
||||
if (loaded.lines) {
|
||||
if (!Array.isArray(loaded.lines)) {
|
||||
@@ -94,7 +119,7 @@ export async function loadSettings(): Promise<Settings> {
|
||||
}
|
||||
loaded.lines = loaded.lines.slice(0, 3);
|
||||
}
|
||||
|
||||
|
||||
return { ...DEFAULT_SETTINGS, ...loaded };
|
||||
} catch (error) {
|
||||
// Any other error, return defaults
|
||||
@@ -106,19 +131,19 @@ export async function loadSettings(): Promise<Settings> {
|
||||
function migrateOldSettings(old: any): Settings {
|
||||
const items: StatusItem[] = [];
|
||||
let id = 1;
|
||||
|
||||
|
||||
if (old.elements?.model) {
|
||||
items.push({ id: String(id++), type: 'model', color: old.colors?.model });
|
||||
}
|
||||
|
||||
|
||||
if (items.length > 0 && old.elements?.gitBranch) {
|
||||
items.push({ id: String(id++), type: 'separator' });
|
||||
}
|
||||
|
||||
|
||||
if (old.elements?.gitBranch) {
|
||||
items.push({ id: String(id++), type: 'git-branch', color: old.colors?.gitBranch });
|
||||
}
|
||||
|
||||
|
||||
if (old.layout?.expandingSeparators) {
|
||||
// Replace regular separators with flex separators
|
||||
items.forEach(item => {
|
||||
@@ -127,7 +152,7 @@ function migrateOldSettings(old: any): Settings {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
lines: [items], // Put migrated items in first line
|
||||
colors: old.colors || DEFAULT_SETTINGS.colors,
|
||||
@@ -139,7 +164,7 @@ function migrateOldSettings(old: any): Settings {
|
||||
export async function saveSettings(settings: Settings): Promise<void> {
|
||||
// Ensure config directory exists
|
||||
await mkdir(CONFIG_DIR, { recursive: true });
|
||||
|
||||
|
||||
// Write settings using Node.js-compatible API
|
||||
await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
}
|
||||
+6
-6
@@ -220,7 +220,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
|
||||
if (width && width > 0) {
|
||||
// Remove ANSI escape codes to get actual length
|
||||
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
|
||||
|
||||
if (plainLength > width) {
|
||||
// Need to truncate - preserve ANSI codes while truncating
|
||||
let truncated = '';
|
||||
@@ -228,10 +228,10 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
|
||||
let inAnsiCode = false;
|
||||
let ansiBuffer = '';
|
||||
const targetLength = width - 5; // Reserve 3 chars for ellipsis + 2 for proper fit in preview
|
||||
|
||||
|
||||
for (let i = 0; i < statusLine.length; i++) {
|
||||
const char = statusLine[i];
|
||||
|
||||
|
||||
if (char === '\x1b') {
|
||||
inAnsiCode = true;
|
||||
ansiBuffer = char;
|
||||
@@ -251,7 +251,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
statusLine = truncated + '...';
|
||||
}
|
||||
}
|
||||
@@ -783,9 +783,9 @@ interface FlexOptionsProps {
|
||||
|
||||
const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack }) => {
|
||||
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode || 'full-minus-40');
|
||||
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 75);
|
||||
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 60);
|
||||
const [editingThreshold, setEditingThreshold] = useState(false);
|
||||
const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold || 75));
|
||||
const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold || 60));
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [highlightedOption, setHighlightedOption] = useState<FlexMode>(settings.flexMode || 'full-minus-40');
|
||||
|
||||
|
||||
Reference in new issue
Block a user