diff --git a/CLAUDE.md b/CLAUDE.md index 9aeff20..6a26da4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,15 @@ The project has dual runtime compatibility - works with both Bun and Node.js: - **claude-settings.ts**: Integration with Claude Code settings.json - **colors.ts**: Color definitions and ANSI code mapping +### Widgets (src/widgets/) +Custom widgets implementing the StatusItemWidget interface: +- Model, Version, OutputStyle - Claude Code metadata display +- GitBranch, GitChanges - Git repository status +- TokensInput, TokensOutput, TokensCached, TokensTotal - Token usage metrics +- ContextLength, ContextPercentage, ContextPercentageUsable - Context window metrics +- BlockTimer, SessionClock - Time tracking +- CurrentWorkingDir, TerminalWidth - Environment info + ## Key Implementation Details - **Cross-platform stdin reading**: Detects Bun vs Node.js environment and uses appropriate stdin API diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..a33f3d3 --- /dev/null +++ b/log.txt @@ -0,0 +1 @@ +{"session_id":"05091387-3173-4179-afdd-fc3a3694b11f","transcript_path":"/Users/sirmalloc/.claude/projects/-Users-sirmalloc-Projects-Personal-ccstatusline/05091387-3173-4179-afdd-fc3a3694b11f.jsonl","cwd":"/Users/sirmalloc/Projects/Personal/ccstatusline","model":{"id":"claude-opus-4-1-20250805","display_name":"Opus 4.1"},"workspace":{"current_dir":"/Users/sirmalloc/Projects/Personal/ccstatusline","project_dir":"/Users/sirmalloc/Projects/Personal/ccstatusline"},"version":"1.0.86","output_style":{"name":"default"},"cost":{"total_cost_usd":5.474908800000001,"total_duration_ms":3466518,"total_api_duration_ms":219805,"total_lines_added":0,"total_lines_removed":0}} \ No newline at end of file diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 9197692..3025624 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -201,7 +201,7 @@ export const App: React.FC = () => { }; return ( - + diff --git a/src/tui/components/ItemsEditor.tsx b/src/tui/components/ItemsEditor.tsx index 1ba8b80..7276ff7 100644 --- a/src/tui/components/ItemsEditor.tsx +++ b/src/tui/components/ItemsEditor.tsx @@ -269,7 +269,7 @@ export const ItemsEditor: React.FC = ({ widgets, onUpdate, onB const customKeybinds = widgetImpl.getCustomKeybinds(); const matchedKeybind = customKeybinds.find(kb => kb.key === input); - if (matchedKeybind) { + if (matchedKeybind && !key.ctrl) { // Check if widget handles the action directly if (widgetImpl.handleEditorAction) { // Let the widget handle the action directly diff --git a/src/types/StatusJSON.ts b/src/types/StatusJSON.ts index 842e92c..7569a5e 100644 --- a/src/types/StatusJSON.ts +++ b/src/types/StatusJSON.ts @@ -14,7 +14,14 @@ export const StatusJSONSchema = z.looseObject({ project_dir: z.string().optional() }).optional(), version: z.string().optional(), - output_style: z.object({ name: z.string().optional() }).optional() + output_style: z.object({ name: z.string().optional() }).optional(), + cost: z.object({ + total_cost_usd: z.number().optional(), + total_duration_ms: z.number().optional(), + total_api_duration_ms: z.number().optional(), + total_lines_added: z.number().optional(), + total_lines_removed: z.number().optional() + }).optional() }); export type StatusJSON = z.infer; \ No newline at end of file diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 3076ccf..92dc48b 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -96,18 +96,18 @@ function renderPowerlineStatusLine( if (context.isPreview) { // In preview mode, account for box borders and padding (6 chars total) if (flexMode === 'full') { - terminalWidth = detectedWidth - 8; + terminalWidth = detectedWidth - 6; } else if (flexMode === 'full-minus-40') { - terminalWidth = detectedWidth - 43; + terminalWidth = detectedWidth - 40; } else if (flexMode === 'full-until-compact') { - terminalWidth = detectedWidth - 8; + terminalWidth = detectedWidth - 6; } } else { // In actual rendering mode if (flexMode === 'full') { - terminalWidth = detectedWidth - 4; + terminalWidth = detectedWidth - 6; } else if (flexMode === 'full-minus-40') { - terminalWidth = detectedWidth - 41; + terminalWidth = detectedWidth - 40; } else if (flexMode === 'full-until-compact') { const threshold = settings.compactThreshold; const contextPercentage = context.tokenMetrics @@ -116,7 +116,7 @@ function renderPowerlineStatusLine( if (contextPercentage >= threshold) { terminalWidth = detectedWidth - 40; } else { - terminalWidth = detectedWidth - 4; + terminalWidth = detectedWidth - 6; } } } @@ -502,7 +502,7 @@ export function renderStatusLine( if (flexMode === 'full') { terminalWidth = detectedWidth - 6; // Subtract 6 for box borders and padding in preview } else if (flexMode === 'full-minus-40') { - terminalWidth = detectedWidth - 43; // -40 for auto-compact + 3 for preview + terminalWidth = detectedWidth - 40; // -40 for auto-compact + 3 for preview } else if (flexMode === 'full-until-compact') { // For preview, always show full width minus preview padding terminalWidth = detectedWidth - 6; @@ -511,10 +511,10 @@ export function renderStatusLine( // In actual rendering mode if (flexMode === 'full') { // Use full width minus 4 for terminal padding - terminalWidth = detectedWidth - 4; + terminalWidth = detectedWidth - 6; } else if (flexMode === 'full-minus-40') { // Always subtract 41 for auto-compact message - terminalWidth = detectedWidth - 41; + terminalWidth = detectedWidth - 40; } else if (flexMode === 'full-until-compact') { // Check context percentage to decide const threshold = settings.compactThreshold; @@ -526,7 +526,7 @@ export function renderStatusLine( terminalWidth = detectedWidth - 40; } else { // Context is low, use full width minus 4 for padding - terminalWidth = detectedWidth - 4; + terminalWidth = detectedWidth - 6; } } } diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts index a21163f..d838ac3 100644 --- a/src/utils/widgets.ts +++ b/src/utils/widgets.ts @@ -11,6 +11,7 @@ const widgetRegistry = new Map([ ['output-style', new widgets.OutputStyleWidget()], ['git-branch', new widgets.GitBranchWidget()], ['git-changes', new widgets.GitChangesWidget()], + ['current-working-dir', new widgets.CurrentWorkingDirWidget()], ['tokens-input', new widgets.TokensInputWidget()], ['tokens-output', new widgets.TokensOutputWidget()], ['tokens-cached', new widgets.TokensCachedWidget()], diff --git a/src/widgets/CurrentWorkingDir.tsx b/src/widgets/CurrentWorkingDir.tsx new file mode 100644 index 0000000..43263ff --- /dev/null +++ b/src/widgets/CurrentWorkingDir.tsx @@ -0,0 +1,134 @@ +import { + Box, + Text, + useInput +} from 'ink'; +import React, { useState } from 'react'; + +import type { RenderContext } from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + CustomKeybind, + Widget, + WidgetEditorDisplay, + WidgetEditorProps, + WidgetItem +} from '../types/Widget'; + +export class CurrentWorkingDirWidget implements Widget { + getDefaultColor(): string { return 'blue'; } + getDescription(): string { return 'Shows the current working directory'; } + getDisplayName(): string { return 'Current Working Dir'; } + getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { + const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined; + const modifiers: string[] = []; + + if (segments && segments > 0) { + modifiers.push(`segments: ${segments}`); + } + + return { + displayText: this.getDisplayName(), + modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined + }; + } + + render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + if (context.isPreview) { + const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined; + if (segments && segments > 0) { + if (segments === 1) { + return 'cwd: .../project'; + } else { + return 'cwd: .../example/project'; + } + } + return 'cwd: /Users/example/project'; + } + + const cwd = context.data?.cwd; + if (!cwd) { + return null; + } + + const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined; + let displayPath = cwd; + + if (segments && segments > 0) { + const pathParts = cwd.split('/'); + // Remove empty strings from splitting (e.g., leading slash creates empty first element) + const filteredParts = pathParts.filter(part => part !== ''); + + if (filteredParts.length > segments) { + // Take the last N segments + const selectedSegments = filteredParts.slice(-segments); + displayPath = '.../' + selectedSegments.join('/'); + } + } + + return `cwd: ${displayPath}`; + } + + getCustomKeybinds(): CustomKeybind[] { + return [ + { key: 's', label: '(s)egments', action: 'edit-segments' } + ]; + } + + renderEditor(props: WidgetEditorProps): React.ReactElement { + return ; + } + + supportsRawValue(): boolean { return false; } + supportsColors(item: WidgetItem): boolean { return true; } +} + +const CurrentWorkingDirEditor: React.FC = ({ widget, onComplete, onCancel, action }) => { + const [segmentsInput, setSegmentsInput] = useState(widget.metadata?.segments ?? ''); + + useInput((input, key) => { + if (action === 'edit-segments') { + if (key.return) { + const segments = parseInt(segmentsInput, 10); + if (!isNaN(segments) && segments > 0) { + onComplete({ + ...widget, + metadata: { + ...widget.metadata, + segments: segments.toString() + } + }); + } else { + // Clear segments if blank or invalid + const { segments, ...restMetadata } = widget.metadata ?? {}; + void segments; // Intentionally unused + onComplete({ + ...widget, + metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined + }); + } + } else if (key.escape) { + onCancel(); + } else if (key.backspace) { + setSegmentsInput(segmentsInput.slice(0, -1)); + } else if (input && /\d/.test(input) && !key.ctrl) { + setSegmentsInput(segmentsInput + input); + } + } + }); + + if (action === 'edit-segments') { + return ( + + + Enter number of segments to display (blank for full path): + {segmentsInput} + {' '} + + Press Enter to save, ESC to cancel + + ); + } + + return Unknown editor mode; +}; \ No newline at end of file diff --git a/src/widgets/CustomCommand.tsx b/src/widgets/CustomCommand.tsx index 572c0a1..38f13ce 100644 --- a/src/widgets/CustomCommand.tsx +++ b/src/widgets/CustomCommand.tsx @@ -216,22 +216,22 @@ const CustomCommandEditor: React.FC = ({ widget, onComplete, } else if (mode === 'width') { return ( - - Enter max width (blank for no limit): - {' '} - {widthInput} - + + Enter max width (blank for no limit): + {widthInput} + {' '} + Press Enter to save, ESC to cancel ); } else if (mode === 'timeout') { return ( - - Enter timeout in milliseconds (default 1000): - {' '} - {timeoutInput} - + + Enter timeout in milliseconds (default 1000): + {timeoutInput} + {' '} + Press Enter to save, ESC to cancel ); diff --git a/src/widgets/index.ts b/src/widgets/index.ts index fc73da1..b6db363 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -14,4 +14,5 @@ export { TerminalWidthWidget } from './TerminalWidth'; export { VersionWidget } from './Version'; export { CustomTextWidget } from './CustomText'; export { CustomCommandWidget } from './CustomCommand'; -export { BlockTimerWidget } from './BlockTimer'; \ No newline at end of file +export { BlockTimerWidget } from './BlockTimer'; +export { CurrentWorkingDirWidget } from './CurrentWorkingDir'; \ No newline at end of file