Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ ccstatusline --version
- **Claude Session ID** / **Session Name** / **Claude Account Email** - Show session identifiers plus the currently signed-in Claude account email.
- **Claude Status** - Show the current Claude/Anthropic service status from `status.claude.com` (`ok` when the indicator is `none`, otherwise the indicator word such as `minor`, `major`, `critical`, or `maintenance`). Press `h` in the editor to add a 48-hour incident-history strip of eight six-hour blocks (oldest to newest), each colored by the worst incident overlapping that block: green (none), yellow (minor), orange (major), red (critical). Responses are cached for about five minutes; on network failure the widget shows stale data when available or degrades to `?`. With the history strip enabled the widget colors itself by severity, so per-widget foreground colors and theme foregrounds are skipped, like Custom Command's preserve-colors mode. Press `l` to make the widget a link to the status page (`https://status.claude.com`), in terminals that support OSC 8 hyperlinks.
- **Voice Status** - Show whether Claude Code voice input is enabled. It can render as an icon, icon plus text, plain text, or `voice on/off`, with optional Nerd Font microphone icons.
- **Fast Mode** - Show whether Claude Code's fast mode is on, from the status line's `fast_mode` field. It can render as icon plus text (`⚡ on` / `⚡ off`, the default), an icon with a state dot, plain text, or `fast on/off`, with optional Nerd Font lightning icons. It can hide while fast mode is off, and shows nothing on Claude Code versions that don't report fast mode.
- **Sandbox Status** - Show the effective `sandbox.enabled` value from Claude Code's layered project and user settings. It can render as a glyph, `SB: ON/OFF`, or `Sandbox: ON/OFF`, with optional Nerd Font lock icons. The value is refreshed after `/sandbox` changes, but is best effort when managed or CLI settings override files or sandbox initialization fails.
- **Thinking Effort** / **Vim Mode** / **Skills** - Show Claude thinking effort, the current vim editing mode, and skill activity from hook data. Thinking Effort reads live status JSON first, then `/model` or `/effort` transcript output, then settings fallback; it supports `low`, `medium`, `high`, `xhigh`, and `max`, shows `default` when no effort is set, and marks unknown future values with `?`. Claude Code reports Ultracode as `xhigh` in status line data; it does not expose Ultracode as a separate effort level.
- **Session Clock** / **Session Cost** / **Session Cost Rate** - Show elapsed session time, the current session cost in USD, and that cost per hour (`Rate: $5.00/hr`). Session Cost Rate divides by the time Claude spent working (API time) by default, or by the whole session with clock time on, and shows nothing until a full minute of that time has passed.
Expand Down Expand Up @@ -309,6 +310,7 @@ Widget-specific shortcuts:
- **Sandbox Status**: `f` cycle glyph/text/word format, `n` toggle Nerd Font lock icons in glyph mode
- **Claude Status**: `h` toggle the 48-hour incident-history strip, `l` toggle the link to the status page
- **Voice Status**: `f` cycle format, `n` toggle Nerd Font microphone icons
- **Fast Mode**: `f` cycle format, `n` toggle Nerd Font lightning icons
- **Current Working Dir**: `h` home abbreviation, `s` segment editor, `f` fish-style path, `g` optional leading glyph (off by default; pair with raw value to replace the `cwd:` label with the glyph)
- **Skills**: `v` cycle view mode, `l` edit list limit in list mode
- **Input Speed / Output Speed / Total Speed**: `w` edit the rolling window in seconds
Expand Down
3 changes: 3 additions & 0 deletions src/types/StatusJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const StatusJSONSchema = z.looseObject({
remaining_percentage: CoercedNumberSchema.nullable().optional()
}).nullable().optional(),
vim: z.object({ mode: z.string().optional() }).nullable().optional(),
// Only the Fast Mode widget reads it, so a malformed value is dropped
// rather than failing the whole payload
fast_mode: z.boolean().nullable().optional().catch(undefined),
worktree: z.object({
name: z.string().optional(),
path: z.string().optional(),
Expand Down
35 changes: 35 additions & 0 deletions src/types/__tests__/StatusJSON.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,39 @@ describe('StatusJSONSchema numeric coercion', () => {
expect(result.data.rate_limits?.spend_limit?.used_percentage).toBe(12);
expect(result.data.rate_limits?.spend_limit?.used_usd).toBeUndefined();
});

it('parses fast_mode', () => {
const result = StatusJSONSchema.safeParse({ fast_mode: true });

expect(result.success).toBe(true);
if (!result.success) {
return;
}

expect(result.data.fast_mode).toBe(true);
});

it('accepts null fast_mode', () => {
const result = StatusJSONSchema.safeParse({ fast_mode: null });

expect(result.success).toBe(true);
if (!result.success) {
return;
}

expect(result.data.fast_mode).toBeNull();
});

// One widget's field must not blank the whole status line
it('drops a malformed fast_mode and keeps the rest of the payload', () => {
const result = StatusJSONSchema.safeParse({ fast_mode: 'yes', model: { id: 'claude-opus-4-6' } });

expect(result.success).toBe(true);
if (!result.success) {
return;
}

expect(result.data.fast_mode).toBeUndefined();
expect(result.data.model).toEqual({ id: 'claude-opus-4-6' });
});
});
1 change: 1 addition & 0 deletions src/utils/widget-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const WIDGET_MANIFEST: WidgetManifestEntry[] = [
{ type: 'thinking-effort', create: () => new widgets.ThinkingEffortWidget() },
{ type: 'vim-mode', create: () => new widgets.VimModeWidget() },
{ type: 'voice-status', create: () => new widgets.VoiceStatusWidget() },
{ type: 'fast-mode', create: () => new widgets.FastModeWidget() },
{ type: 'remote-control-status', create: () => new widgets.RemoteControlStatusWidget() },
{ type: 'worktree-mode', create: () => new widgets.GitWorktreeModeWidget() },
{ type: 'worktree-name', create: () => new widgets.GitWorktreeNameWidget() },
Expand Down
119 changes: 119 additions & 0 deletions src/widgets/FastMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
HideableState,
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';

import {
getFormat,
getFormatKeybinds,
getFormatModifierText,
handleFormatAction,
type FormatOptions
} from './shared/format-options';
import { isHidden } from './shared/hideable';
import { isNerdFontEnabled } from './shared/metadata';

const BOLT_EMOJI = '⚡';
const FLASH_NERD_FONT = '󰉁'; // nf-md-flash (U+F0241)
const FLASH_OFF_NERD_FONT = '󰉃'; // nf-md-flash_off (U+F0243)
const STATE_DOT_OFF = '○';
const STATE_DOT_ON = '◉';

const FORMATS = ['icon-text', 'icon', 'text', 'word'] as const;
type FastModeFormat = typeof FORMATS[number];

const DEFAULT_FORMAT: FastModeFormat = 'icon-text';

const OFF_HIDEABLE_STATE: HideableState = { key: 'off', label: 'when fast mode is off' };

function canUseNerdFont(item: WidgetItem): boolean {
const format = getFormat(item, FORMAT_OPTIONS);
return format === 'icon' || (format === 'icon-text' && !item.rawValue);
}

const FORMAT_OPTIONS: FormatOptions<FastModeFormat> = {
formats: FORMATS,
defaultFormat: DEFAULT_FORMAT,
canUseNerdFont
};

function getIcon(enabled: boolean, nerdFont: boolean): string {
if (!nerdFont) {
return BOLT_EMOJI;
}
return enabled ? FLASH_NERD_FONT : FLASH_OFF_NERD_FONT;
}

function formatStatus(enabled: boolean, format: FastModeFormat, nerdFont: boolean, rawValue: boolean): string {
const stateText = enabled ? 'on' : 'off';
const stateDot = enabled ? STATE_DOT_ON : STATE_DOT_OFF;
const icon = getIcon(enabled, nerdFont);

switch (format) {
case 'icon-text':
return rawValue ? stateText : `${icon} ${stateText}`;
case 'icon':
if (nerdFont) {
return icon;
}
return rawValue ? stateDot : `${icon} ${stateDot}`;
case 'text':
return stateText;
case 'word':
return rawValue ? stateText : `fast ${stateText}`;
}
}

export class FastModeWidget implements Widget {
getDefaultColor(): string { return 'yellow'; }
getDescription(): string { return 'Shows whether Claude Code fast mode is on'; }
getDisplayName(): string { return 'Fast Mode'; }
getCategory(): string { return 'Core'; }

getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return {
displayText: this.getDisplayName(),
modifierText: getFormatModifierText(item, FORMAT_OPTIONS)
};
}

getHideableStates(): HideableState[] {
return [OFF_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
return handleFormatAction(action, item, FORMAT_OPTIONS);
}

render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
const format = getFormat(item, FORMAT_OPTIONS);
const nerdFont = isNerdFontEnabled(item, FORMAT_OPTIONS);

if (context.isPreview) {
return formatStatus(true, format, nerdFont, item.rawValue ?? false);
}

// Absent before Claude Code reported it
const enabled = context.data?.fast_mode;
if (typeof enabled !== 'boolean') {
return null;
}
if (!enabled && isHidden(item, OFF_HIDEABLE_STATE.key)) {
return null;
}

return formatStatus(enabled, format, nerdFont, item.rawValue ?? false);
}

getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getFormatKeybinds(item, FORMAT_OPTIONS);
}

supportsRawValue(): boolean { return true; }
supportsColors(_item: WidgetItem): boolean { return true; }
}
Loading
Loading