ccstatusline 2.2.10 โ†’ 2.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@
47
47
 
48
48
  ## ๐Ÿ†• Recent Updates
49
49
 
50
- ### v2.2.9 - v2.2.10 - GitLab support, reset timers, context, and git widgets
50
+ ### v2.2.9 - v2.2.11 - GitLab support, reset timers, context, compaction, and git widgets
51
51
 
52
52
  - **๐ŸฆŠ GitLab PR/MR support** - `Git Branch` and `Git PR/MR` now support GitHub, GitLab, and compatible self-hosted remotes, using `gh` or `glab` as appropriate.
53
53
  - **๐Ÿ”„ Status line refresh interval** - Installed configs can set Claude Code's `statusLine.refreshInterval` from the TUI when Claude Code >=2.1.97 supports it.
@@ -56,10 +56,12 @@
56
56
  - **๐Ÿ“Š Short bar display modes** - Context percentage, Context Bar, Session Usage, Weekly Usage, Block Timer, and reset timer widgets can use compact bar variants.
57
57
  - **๐Ÿ•’ Reset timer timestamps** - Block and Weekly Reset Timer widgets can show exact reset timestamps with compact formatting, 12/24-hour display, IANA time zones, and locale selection.
58
58
  - **๐ŸชŸ Context Window widget** - Added a `Context Window` widget for total model window size, keeping `Context Length` focused on current context usage.
59
+ - **๐Ÿ” Compaction Counter widget** - Added a `Compaction Counter` widget that tracks session context compactions, with icon/text/number formats, optional Nerd Font icon, and hide-when-zero behavior.
59
60
  - **๐Ÿงฎ Git file status widgets** - Added `Git Staged Files`, `Git Unstaged Files`, `Git Untracked Files`, and `Git Clean Status` for file counts and clean/dirty state.
60
- - **๐Ÿง  Thinking Effort updates** - Added `xhigh`, show `default` when no effort is set, and mark unknown future effort levels with `?`.
61
+ - **๐Ÿง  Thinking Effort updates** - Added `xhigh`, show `default` when no effort is set, mark unknown future effort levels with `?`, and track live status JSON plus `/effort` command changes.
61
62
  - **๐Ÿงฎ More accurate token counts** - Streaming duplicate JSONL entries are deduped so token widgets do not overcount live Claude Code output.
62
63
  - **๐Ÿท๏ธ Cleaner model display** - The Model widget strips trailing context suffixes like `(1M context)`; use `Context Window` when you want the total window size shown.
64
+ - **๐Ÿงน Cleaner empty-widget separators** - Manual separators now collapse around widgets that render empty, avoiding dangling separators when hide-when-empty widgets disappear.
63
65
  - **๐Ÿงฑ More resilient Git helpers** - Git widgets handle missing or unusual git command output more defensively.
64
66
 
65
67
  ### v2.2.8 - Git widgets, smarter picker search, and minimalist mode
@@ -192,7 +194,7 @@
192
194
 
193
195
  ## โœจ Features
194
196
 
195
- - **๐Ÿ“Š Real-time Metrics** - Display model name, git branch, token usage, session duration, block timer, and more
197
+ - **๐Ÿ“Š Real-time Metrics** - Display model name, git branch, token usage, session duration, compaction count, block timer, and more
196
198
  - **๐ŸŽจ Fully Customizable** - Choose what to display and customize colors for each element
197
199
  - **โšก Powerline Support** - Beautiful Powerline-style rendering with arrow separators, caps, and custom fonts
198
200
  - **๐Ÿ“ Multi-line Support** - Configure multiple independent status lines
@@ -56010,17 +56010,26 @@ function getContextConfig(modelIdentifier, contextWindowSize) {
56010
56010
  var DEFAULT_CONTEXT_WINDOW_SIZE = 200000, USABLE_CONTEXT_RATIO = 0.8;
56011
56011
 
56012
56012
  // src/utils/context-percentage.ts
56013
- function calculateContextPercentage(context) {
56013
+ function calculateContextPercentageMetrics(context) {
56014
56014
  const contextWindowMetrics = getContextWindowMetrics(context.data);
56015
+ const modelIdentifier = getModelContextIdentifier(context.data?.model);
56016
+ const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize);
56015
56017
  if (contextWindowMetrics.usedPercentage !== null) {
56016
- return contextWindowMetrics.usedPercentage;
56018
+ return {
56019
+ usedPercentage: contextWindowMetrics.usedPercentage,
56020
+ windowSize: contextConfig.maxTokens
56021
+ };
56017
56022
  }
56018
56023
  if (!context.tokenMetrics) {
56019
- return 0;
56024
+ return null;
56020
56025
  }
56021
- const modelIdentifier = getModelContextIdentifier(context.data?.model);
56022
- const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize);
56023
- return Math.min(100, context.tokenMetrics.contextLength / contextConfig.maxTokens * 100);
56026
+ return {
56027
+ usedPercentage: Math.min(100, context.tokenMetrics.contextLength / contextConfig.maxTokens * 100),
56028
+ windowSize: contextConfig.maxTokens
56029
+ };
56030
+ }
56031
+ function calculateContextPercentage(context) {
56032
+ return calculateContextPercentageMetrics(context)?.usedPercentage ?? 0;
56024
56033
  }
56025
56034
  var init_context_percentage = () => {};
56026
56035
 
@@ -56127,7 +56136,7 @@ function getTerminalWidth() {
56127
56136
  function canDetectTerminalWidth() {
56128
56137
  return probeTerminalWidth() !== null;
56129
56138
  }
56130
- var __dirname = "/Users/sirmalloc/Projects/Personal/ccstatusline/src/utils", PACKAGE_VERSION = "2.2.10";
56139
+ var __dirname = "/Users/sirmalloc/Projects/Personal/ccstatusline/src/utils", PACKAGE_VERSION = "2.2.11";
56131
56140
  var init_terminal = () => {};
56132
56141
 
56133
56142
  // src/utils/renderer.ts
@@ -56532,12 +56541,12 @@ function renderStatusLine(widgets, settings, context, preRenderedWidgets, preCal
56532
56541
  let hasContentBefore = false;
56533
56542
  for (let j = i - 1;j >= 0; j--) {
56534
56543
  const prevWidget = widgets[j];
56535
- if (prevWidget && prevWidget.type !== "separator" && prevWidget.type !== "flex-separator") {
56536
- if (preRenderedWidgets[j]?.content) {
56537
- hasContentBefore = true;
56538
- break;
56539
- }
56540
- }
56544
+ if (!prevWidget)
56545
+ continue;
56546
+ if (prevWidget.type === "separator" || prevWidget.type === "flex-separator")
56547
+ continue;
56548
+ hasContentBefore = Boolean(preRenderedWidgets[j]?.content);
56549
+ break;
56541
56550
  }
56542
56551
  if (!hasContentBefore)
56543
56552
  continue;
@@ -57378,7 +57387,7 @@ class ContextPercentageWidget {
57378
57387
  render(item, context, settings) {
57379
57388
  const isInverse = isContextInverse(item);
57380
57389
  const sliderMode = getContextSliderMode(item);
57381
- const contextWindowMetrics = getContextWindowMetrics(context.data);
57390
+ const contextPercentageMetrics = calculateContextPercentageMetrics(context);
57382
57391
  if (context.isPreview) {
57383
57392
  const previewPercent = isInverse ? 90.7 : 9.3;
57384
57393
  const sliderResult = renderContextSlider(sliderMode, previewPercent);
@@ -57387,19 +57396,8 @@ class ContextPercentageWidget {
57387
57396
  }
57388
57397
  return formatRawOrLabeledValue(item, "Ctx: ", `${previewPercent.toFixed(1)}%`);
57389
57398
  }
57390
- if (contextWindowMetrics.usedPercentage !== null) {
57391
- const displayPercentage = isInverse ? 100 - contextWindowMetrics.usedPercentage : contextWindowMetrics.usedPercentage;
57392
- const sliderResult = renderContextSlider(sliderMode, displayPercentage);
57393
- if (sliderResult !== null) {
57394
- return formatRawOrLabeledValue(item, "Ctx: ", sliderResult);
57395
- }
57396
- return formatRawOrLabeledValue(item, "Ctx: ", `${displayPercentage.toFixed(1)}%`);
57397
- }
57398
- if (context.tokenMetrics) {
57399
- const modelIdentifier = getModelContextIdentifier(context.data?.model);
57400
- const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize);
57401
- const usedPercentage = Math.min(100, context.tokenMetrics.contextLength / contextConfig.maxTokens * 100);
57402
- const displayPercentage = isInverse ? 100 - usedPercentage : usedPercentage;
57399
+ if (contextPercentageMetrics !== null) {
57400
+ const displayPercentage = isInverse ? 100 - contextPercentageMetrics.usedPercentage : contextPercentageMetrics.usedPercentage;
57403
57401
  const sliderResult = renderContextSlider(sliderMode, displayPercentage);
57404
57402
  if (sliderResult !== null) {
57405
57403
  return formatRawOrLabeledValue(item, "Ctx: ", sliderResult);
@@ -57422,6 +57420,7 @@ class ContextPercentageWidget {
57422
57420
  }
57423
57421
  }
57424
57422
  var init_ContextPercentage = __esm(() => {
57423
+ init_context_percentage();
57425
57424
  init_context_inverse();
57426
57425
  init_context_slider();
57427
57426
  });
@@ -63063,6 +63062,12 @@ function getTranscriptThinkingEffort(transcriptPath) {
63063
63062
  continue;
63064
63063
  }
63065
63064
  const visibleContent = getVisibleText(entry.message.content).trim();
63065
+ if (visibleContent.startsWith(EFFORT_STDOUT_PREFIX)) {
63066
+ const effortMatch = EFFORT_STDOUT_REGEX.exec(visibleContent);
63067
+ if (effortMatch) {
63068
+ return normalizeThinkingEffort(effortMatch[1]);
63069
+ }
63070
+ }
63066
63071
  if (!visibleContent.startsWith(MODEL_STDOUT_PREFIX)) {
63067
63072
  continue;
63068
63073
  }
@@ -63074,13 +63079,14 @@ function getTranscriptThinkingEffort(transcriptPath) {
63074
63079
  }
63075
63080
  return;
63076
63081
  }
63077
- var KNOWN_THINKING_EFFORTS, KNOWN_THINKING_EFFORTS_SET, MODEL_STDOUT_PREFIX = "<local-command-stdout>Set model to ", MODEL_STDOUT_EFFORT_REGEX, UNKNOWN_EFFORT_PATTERN;
63082
+ var KNOWN_THINKING_EFFORTS, KNOWN_THINKING_EFFORTS_SET, MODEL_STDOUT_PREFIX = "<local-command-stdout>Set model to ", MODEL_STDOUT_EFFORT_REGEX, EFFORT_STDOUT_PREFIX = "<local-command-stdout>Set effort level to ", EFFORT_STDOUT_REGEX, UNKNOWN_EFFORT_PATTERN;
63078
63083
  var init_jsonl_metadata = __esm(() => {
63079
63084
  init_ansi();
63080
63085
  init_jsonl_lines();
63081
63086
  KNOWN_THINKING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
63082
63087
  KNOWN_THINKING_EFFORTS_SET = new Set(KNOWN_THINKING_EFFORTS);
63083
63088
  MODEL_STDOUT_EFFORT_REGEX = /^<local-command-stdout>Set model to[\s\S]*? with ([a-zA-Z0-9-]+) effort<\/local-command-stdout>$/i;
63089
+ EFFORT_STDOUT_REGEX = /^<local-command-stdout>Set effort level to ([a-zA-Z0-9-]+)\b/i;
63084
63090
  UNKNOWN_EFFORT_PATTERN = /^(?=.*[a-z0-9])[a-z0-9-]{2,20}$/;
63085
63091
  });
63086
63092
 
@@ -65587,6 +65593,13 @@ var init_Skills = __esm(async () => {
65587
65593
  });
65588
65594
 
65589
65595
  // src/widgets/ThinkingEffort.ts
65596
+ function resolveThinkingEffortFromStatusJson(context) {
65597
+ const effort = context.data?.effort;
65598
+ if (!effort || !("level" in effort)) {
65599
+ return;
65600
+ }
65601
+ return typeof effort.level === "string" ? normalizeThinkingEffort(effort.level) : null;
65602
+ }
65590
65603
  function resolveThinkingEffortFromSettings() {
65591
65604
  try {
65592
65605
  const settings = loadClaudeSettingsSync({ logErrors: false });
@@ -65595,6 +65608,10 @@ function resolveThinkingEffortFromSettings() {
65595
65608
  return;
65596
65609
  }
65597
65610
  function resolveThinkingEffort(context) {
65611
+ const statusEffort = resolveThinkingEffortFromStatusJson(context);
65612
+ if (statusEffort !== undefined) {
65613
+ return statusEffort;
65614
+ }
65598
65615
  return getTranscriptThinkingEffort(context.data?.transcript_path) ?? resolveThinkingEffortFromSettings() ?? null;
65599
65616
  }
65600
65617
  function formatEffort(resolved) {
@@ -65893,6 +65910,149 @@ class GitWorktreeOriginalBranchWidget {
65893
65910
  }
65894
65911
  }
65895
65912
 
65913
+ // src/widgets/CompactionCounter.ts
65914
+ function getFormat2(item) {
65915
+ const format = item.metadata?.format;
65916
+ return FORMATS2.includes(format ?? "") ? format : DEFAULT_FORMAT2;
65917
+ }
65918
+ function removeNerdFont(item) {
65919
+ const { [NERD_FONT_METADATA_KEY2]: removedNerdFont, ...restMetadata } = item.metadata ?? {};
65920
+ return {
65921
+ ...item,
65922
+ metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
65923
+ };
65924
+ }
65925
+ function setFormat2(item, format) {
65926
+ if (format === DEFAULT_FORMAT2) {
65927
+ const { format: removedFormat, ...restMetadata2 } = item.metadata ?? {};
65928
+ return {
65929
+ ...item,
65930
+ metadata: Object.keys(restMetadata2).length > 0 ? restMetadata2 : undefined
65931
+ };
65932
+ }
65933
+ const { [NERD_FONT_METADATA_KEY2]: removedNerdFont, ...restMetadata } = item.metadata ?? {};
65934
+ return {
65935
+ ...item,
65936
+ metadata: {
65937
+ ...restMetadata,
65938
+ format
65939
+ }
65940
+ };
65941
+ }
65942
+ function isNerdFontEnabled2(item) {
65943
+ return item.metadata?.[NERD_FONT_METADATA_KEY2] === "true" && getFormat2(item) === DEFAULT_FORMAT2;
65944
+ }
65945
+ function isHideZeroEnabled(item) {
65946
+ return item.metadata?.[HIDE_ZERO_METADATA_KEY] === "true";
65947
+ }
65948
+ function toggleHideZero(item) {
65949
+ return {
65950
+ ...item,
65951
+ metadata: {
65952
+ ...item.metadata ?? {},
65953
+ [HIDE_ZERO_METADATA_KEY]: (!isHideZeroEnabled(item)).toString()
65954
+ }
65955
+ };
65956
+ }
65957
+ function toggleNerdFont2(item) {
65958
+ if (getFormat2(item) !== DEFAULT_FORMAT2) {
65959
+ return removeNerdFont(item);
65960
+ }
65961
+ if (!isNerdFontEnabled2(item)) {
65962
+ return {
65963
+ ...item,
65964
+ metadata: {
65965
+ ...item.metadata ?? {},
65966
+ [NERD_FONT_METADATA_KEY2]: "true"
65967
+ }
65968
+ };
65969
+ }
65970
+ return removeNerdFont(item);
65971
+ }
65972
+ function formatCount(count, format, icon) {
65973
+ switch (format) {
65974
+ case "icon-space-number":
65975
+ return `${icon} ${count}`;
65976
+ case "text-and-number":
65977
+ return `Compactions: ${count}`;
65978
+ case "number":
65979
+ return String(count);
65980
+ }
65981
+ }
65982
+
65983
+ class CompactionCounterWidget {
65984
+ getDefaultColor() {
65985
+ return "yellow";
65986
+ }
65987
+ getDescription() {
65988
+ return "Count of context compaction events in the current session.";
65989
+ }
65990
+ getDisplayName() {
65991
+ return "Compaction Counter";
65992
+ }
65993
+ getCategory() {
65994
+ return "Context";
65995
+ }
65996
+ getEditorDisplay(item) {
65997
+ const modifiers = [getFormat2(item)];
65998
+ if (isNerdFontEnabled2(item)) {
65999
+ modifiers.push("nerd font");
66000
+ }
66001
+ if (isHideZeroEnabled(item)) {
66002
+ modifiers.push("hide zero");
66003
+ }
66004
+ return {
66005
+ displayText: "Compaction Counter",
66006
+ modifierText: `(${modifiers.join(", ")})`
66007
+ };
66008
+ }
66009
+ handleEditorAction(action, item) {
66010
+ if (action === CYCLE_FORMAT_ACTION2) {
66011
+ const currentFormat = getFormat2(item);
66012
+ const nextFormat = FORMATS2[(FORMATS2.indexOf(currentFormat) + 1) % FORMATS2.length] ?? DEFAULT_FORMAT2;
66013
+ return setFormat2(item, nextFormat);
66014
+ }
66015
+ if (action === TOGGLE_HIDE_ZERO_ACTION) {
66016
+ return toggleHideZero(item);
66017
+ }
66018
+ if (action === TOGGLE_NERD_FONT_ACTION2) {
66019
+ return toggleNerdFont2(item);
66020
+ }
66021
+ return null;
66022
+ }
66023
+ render(item, context, settings) {
66024
+ const format = getFormat2(item);
66025
+ const icon = isNerdFontEnabled2(item) ? COMPACTION_NERD_FONT_ICON : COMPACTION_ICON;
66026
+ if (context.isPreview) {
66027
+ return formatCount(2, format, icon);
66028
+ }
66029
+ const count = context.compactionData?.count ?? 0;
66030
+ if (count === 0 && isHideZeroEnabled(item))
66031
+ return null;
66032
+ return formatCount(count, format, icon);
66033
+ }
66034
+ getCustomKeybinds(item) {
66035
+ const keybinds = [
66036
+ { key: "f", label: "(f)ormat", action: CYCLE_FORMAT_ACTION2 }
66037
+ ];
66038
+ if (item === undefined || getFormat2(item) === DEFAULT_FORMAT2) {
66039
+ keybinds.push({ key: "n", label: "(n)erd font", action: TOGGLE_NERD_FONT_ACTION2 });
66040
+ }
66041
+ keybinds.push({ key: "h", label: "(h)ide when zero", action: TOGGLE_HIDE_ZERO_ACTION });
66042
+ return keybinds;
66043
+ }
66044
+ supportsRawValue() {
66045
+ return false;
66046
+ }
66047
+ supportsColors(item) {
66048
+ return true;
66049
+ }
66050
+ }
66051
+ var COMPACTION_ICON = "โ†ป", COMPACTION_NERD_FONT_ICON = "๏€ก", FORMATS2, DEFAULT_FORMAT2 = "icon-space-number", CYCLE_FORMAT_ACTION2 = "cycle-format", TOGGLE_HIDE_ZERO_ACTION = "toggle-hide-zero", TOGGLE_NERD_FONT_ACTION2 = "toggle-nerd-font", HIDE_ZERO_METADATA_KEY = "hideZero", NERD_FONT_METADATA_KEY2 = "nerdFont";
66052
+ var init_CompactionCounter = __esm(() => {
66053
+ FORMATS2 = ["icon-space-number", "text-and-number", "number"];
66054
+ });
66055
+
65896
66056
  // src/widgets/index.ts
65897
66057
  var init_widgets = __esm(async () => {
65898
66058
  init_GitBranch();
@@ -65926,6 +66086,7 @@ var init_widgets = __esm(async () => {
65926
66086
  init_FreeMemory();
65927
66087
  init_SessionName();
65928
66088
  init_VimMode();
66089
+ init_CompactionCounter();
65929
66090
  await __promiseAll([
65930
66091
  init_TokensInput(),
65931
66092
  init_TokensOutput(),
@@ -66021,7 +66182,8 @@ var init_widget_manifest = __esm(async () => {
66021
66182
  { type: "worktree-mode", create: () => new GitWorktreeModeWidget },
66022
66183
  { type: "worktree-name", create: () => new GitWorktreeNameWidget },
66023
66184
  { type: "worktree-branch", create: () => new GitWorktreeBranchWidget },
66024
- { type: "worktree-original-branch", create: () => new GitWorktreeOriginalBranchWidget }
66185
+ { type: "worktree-original-branch", create: () => new GitWorktreeOriginalBranchWidget },
66186
+ { type: "compaction-counter", create: () => new CompactionCounterWidget }
66025
66187
  ];
66026
66188
  LAYOUT_WIDGET_MANIFEST = [
66027
66189
  {
@@ -72668,6 +72830,7 @@ var StatusJSONSchema = exports_external.looseObject({
72668
72830
  }).optional(),
72669
72831
  version: exports_external.string().optional(),
72670
72832
  output_style: exports_external.object({ name: exports_external.string().optional() }).optional(),
72833
+ effort: exports_external.object({ level: exports_external.string().nullable().optional() }).nullable().optional(),
72671
72834
  cost: exports_external.object({
72672
72835
  total_cost_usd: CoercedNumberSchema.optional(),
72673
72836
  total_duration_ms: CoercedNumberSchema.optional(),
@@ -72708,6 +72871,115 @@ var StatusJSONSchema = exports_external.looseObject({
72708
72871
  // src/ccstatusline.ts
72709
72872
  init_ansi();
72710
72873
  init_colors();
72874
+
72875
+ // src/utils/compaction.ts
72876
+ init_zod();
72877
+ import * as crypto from "crypto";
72878
+ import * as fs13 from "fs";
72879
+ import * as os13 from "os";
72880
+ import * as path10 from "path";
72881
+ var DEFAULT_DROP_THRESHOLD = 2;
72882
+ var FRESH_PREV_CTX_PCT = -1;
72883
+ var MAX_CACHE_FILE_BYTES = 4096;
72884
+ var SESSION_ID_HASH_HEX_LEN = 32;
72885
+ var FRESH = { count: 0, prevCtxPct: FRESH_PREV_CTX_PCT };
72886
+ var CompactionStateSchema = exports_external.object({
72887
+ count: exports_external.number().int().nonnegative().default(0),
72888
+ prevCtxPct: exports_external.number().default(FRESH_PREV_CTX_PCT),
72889
+ prevWindowSize: exports_external.number().positive().nullable().optional()
72890
+ });
72891
+ function normalizeWindowSize(value) {
72892
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
72893
+ return null;
72894
+ }
72895
+ return value;
72896
+ }
72897
+ function normalizeOptions(options) {
72898
+ if (typeof options === "number") {
72899
+ return { dropThreshold: options, windowSize: null };
72900
+ }
72901
+ const dropThreshold = typeof options.dropThreshold === "number" && Number.isFinite(options.dropThreshold) ? options.dropThreshold : DEFAULT_DROP_THRESHOLD;
72902
+ return { dropThreshold, windowSize: options.windowSize ?? null };
72903
+ }
72904
+ function detectCompaction(currentCtxPct, state, options = DEFAULT_DROP_THRESHOLD) {
72905
+ if (!Number.isFinite(currentCtxPct) || currentCtxPct < 0) {
72906
+ return state;
72907
+ }
72908
+ const { dropThreshold, windowSize } = normalizeOptions(options);
72909
+ const currentWindowSize = normalizeWindowSize(windowSize);
72910
+ const prevWindowSize = normalizeWindowSize(state.prevWindowSize);
72911
+ let { count } = state;
72912
+ const { prevCtxPct } = state;
72913
+ const hasKnownWindowChange = currentWindowSize !== null && prevWindowSize !== null && currentWindowSize !== prevWindowSize;
72914
+ const isLearningWindowSize = currentWindowSize !== null && prevWindowSize === null && prevCtxPct >= 0;
72915
+ if (!hasKnownWindowChange && !isLearningWindowSize && prevCtxPct >= 0 && currentCtxPct < prevCtxPct - dropThreshold) {
72916
+ count += 1;
72917
+ }
72918
+ return {
72919
+ count,
72920
+ prevCtxPct: currentCtxPct,
72921
+ ...currentWindowSize !== null ? { prevWindowSize: currentWindowSize } : {}
72922
+ };
72923
+ }
72924
+ function getCacheDir2() {
72925
+ return path10.join(os13.homedir(), ".cache", "ccstatusline", "compaction");
72926
+ }
72927
+ function sanitizeSessionId(sessionId) {
72928
+ const sanitized = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
72929
+ if (!sanitized || sanitized !== sessionId) {
72930
+ return crypto.createHash("sha256").update(sessionId).digest("hex").slice(0, SESSION_ID_HASH_HEX_LEN);
72931
+ }
72932
+ return sanitized;
72933
+ }
72934
+ function getStatePath(sessionId) {
72935
+ return path10.join(getCacheDir2(), `compaction-${sanitizeSessionId(sessionId)}.json`);
72936
+ }
72937
+ function loadCompactionState(sessionId) {
72938
+ const statePath = getStatePath(sessionId);
72939
+ let fd = null;
72940
+ try {
72941
+ fd = fs13.openSync(statePath, fs13.constants.O_RDONLY | fs13.constants.O_NOFOLLOW);
72942
+ const stats = fs13.fstatSync(fd);
72943
+ if (!stats.isFile() || stats.size > MAX_CACHE_FILE_BYTES) {
72944
+ return FRESH;
72945
+ }
72946
+ const raw = JSON.parse(fs13.readFileSync(fd, "utf-8"));
72947
+ const result2 = CompactionStateSchema.safeParse(raw);
72948
+ return result2.success ? result2.data : FRESH;
72949
+ } catch {
72950
+ return FRESH;
72951
+ } finally {
72952
+ if (fd !== null) {
72953
+ try {
72954
+ fs13.closeSync(fd);
72955
+ } catch {}
72956
+ }
72957
+ }
72958
+ }
72959
+ function saveCompactionState(sessionId, state) {
72960
+ let tmpPath = null;
72961
+ try {
72962
+ const dir = getCacheDir2();
72963
+ if (!fs13.existsSync(dir)) {
72964
+ fs13.mkdirSync(dir, { recursive: true });
72965
+ }
72966
+ const targetPath = getStatePath(sessionId);
72967
+ tmpPath = `${targetPath}.tmp.${process.pid}.${crypto.randomBytes(4).toString("hex")}`;
72968
+ fs13.writeFileSync(tmpPath, JSON.stringify(state) + `
72969
+ `);
72970
+ fs13.renameSync(tmpPath, targetPath);
72971
+ tmpPath = null;
72972
+ } catch {
72973
+ if (tmpPath !== null) {
72974
+ try {
72975
+ fs13.unlinkSync(tmpPath);
72976
+ } catch {}
72977
+ }
72978
+ }
72979
+ }
72980
+
72981
+ // src/ccstatusline.ts
72982
+ init_context_percentage();
72711
72983
  await __promiseAll([
72712
72984
  init_config(),
72713
72985
  init_jsonl()
@@ -72715,23 +72987,23 @@ await __promiseAll([
72715
72987
  await init_renderer2();
72716
72988
 
72717
72989
  // src/utils/skills.ts
72718
- import * as fs13 from "fs";
72719
- import * as os13 from "os";
72720
- import * as path10 from "path";
72990
+ import * as fs14 from "fs";
72991
+ import * as os14 from "os";
72992
+ import * as path11 from "path";
72721
72993
  var EMPTY = { totalInvocations: 0, uniqueSkills: [], lastSkill: null };
72722
72994
  function getSkillsDir() {
72723
- return path10.join(os13.homedir(), ".cache", "ccstatusline", "skills");
72995
+ return path11.join(os14.homedir(), ".cache", "ccstatusline", "skills");
72724
72996
  }
72725
72997
  function getSkillsFilePath(sessionId) {
72726
- return path10.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
72998
+ return path11.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
72727
72999
  }
72728
73000
  function getSkillsMetrics(sessionId) {
72729
73001
  const filePath = getSkillsFilePath(sessionId);
72730
- if (!fs13.existsSync(filePath)) {
73002
+ if (!fs14.existsSync(filePath)) {
72731
73003
  return EMPTY;
72732
73004
  }
72733
73005
  try {
72734
- const invocations = fs13.readFileSync(filePath, "utf-8").trim().split(`
73006
+ const invocations = fs14.readFileSync(filePath, "utf-8").trim().split(`
72735
73007
  `).filter((line) => line.trim()).map((line) => {
72736
73008
  try {
72737
73009
  return JSON.parse(line);
@@ -72880,6 +73152,20 @@ async function renderMultipleLines(data) {
72880
73152
  if (data.session_id) {
72881
73153
  skillsMetrics = getSkillsMetrics(data.session_id);
72882
73154
  }
73155
+ let compactionCount = 0;
73156
+ const hasCompactionWidget = lines.some((line) => line.some((item) => item.type === "compaction-counter"));
73157
+ if (hasCompactionWidget && data.session_id) {
73158
+ const prevState = loadCompactionState(data.session_id);
73159
+ compactionCount = prevState.count;
73160
+ const contextPercentageMetrics = calculateContextPercentageMetrics({ data, tokenMetrics });
73161
+ if (contextPercentageMetrics !== null) {
73162
+ const newState = detectCompaction(contextPercentageMetrics.usedPercentage, prevState, { windowSize: contextPercentageMetrics.windowSize });
73163
+ if (newState.count !== prevState.count || newState.prevCtxPct !== prevState.prevCtxPct || newState.prevWindowSize !== prevState.prevWindowSize) {
73164
+ saveCompactionState(data.session_id, newState);
73165
+ }
73166
+ compactionCount = newState.count;
73167
+ }
73168
+ }
72883
73169
  const context = {
72884
73170
  data,
72885
73171
  tokenMetrics,
@@ -72888,6 +73174,7 @@ async function renderMultipleLines(data) {
72888
73174
  usageData,
72889
73175
  sessionDuration,
72890
73176
  skillsMetrics,
73177
+ compactionData: hasCompactionWidget ? { count: compactionCount } : null,
72891
73178
  isPreview: false,
72892
73179
  minimalist: settings.minimalistMode
72893
73180
  };
@@ -72974,16 +73261,16 @@ async function handleHook() {
72974
73261
  return;
72975
73262
  }
72976
73263
  const filePath = getSkillsFilePath(sessionId);
72977
- const fs14 = await import("fs");
72978
- const path11 = await import("path");
72979
- fs14.mkdirSync(path11.dirname(filePath), { recursive: true });
73264
+ const fs15 = await import("fs");
73265
+ const path12 = await import("path");
73266
+ fs15.mkdirSync(path12.dirname(filePath), { recursive: true });
72980
73267
  const entry = JSON.stringify({
72981
73268
  timestamp: new Date().toISOString(),
72982
73269
  session_id: sessionId,
72983
73270
  skill: skillName,
72984
73271
  source: data.hook_event_name
72985
73272
  });
72986
- fs14.appendFileSync(filePath, entry + `
73273
+ fs15.appendFileSync(filePath, entry + `
72987
73274
  `);
72988
73275
  } catch {}
72989
73276
  console.log("{}");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstatusline",
3
- "version": "2.2.10",
3
+ "version": "2.2.11",
4
4
  "description": "A customizable status line formatter for Claude Code CLI",
5
5
  "module": "src/ccstatusline.ts",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  "strip-ansi": "^7.1.0",
46
46
  "tinyglobby": "^0.2.14",
47
47
  "typedoc": "^0.28.12",
48
- "typescript": "^5.9.2",
48
+ "typescript": "^6.0.2",
49
49
  "typescript-eslint": "^8.39.1",
50
50
  "vitest": "^4.0.18",
51
51
  "zod": "^4.0.17"