Change default flex mode compaction to 60%, make sensible default status line when no config present

This commit is contained in:
Matthew Breedlove committed 2025-08-09 08:38:57 -04:00
1 parent d9dd43a15c
commit 7d7dd2084b
4 files changed
+81 -56

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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');