ccstatusline 2.0.26 → 2.0.28

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
@@ -46,6 +46,19 @@
46
46
 
47
47
  ## 🆕 Recent Updates
48
48
 
49
+ ### v2.0.26 - v2.0.28 - Performance, git internals, and workflow improvements
50
+
51
+ - **⚡ Block timer cache (v2.0.28)** - Cache block timer metrics to reduce JSONL parsing on every render, with per-config hashed cache files and automatic 5-hour block invalidation.
52
+ - **🧱 Git widget command refactor (v2.0.28)** - Refactored git widgets to use shared git command helpers and expanded coverage for failure and edge-case tests.
53
+ - **🪟 Windows UTF-8 piped output fix (v2.0.28)** - Sets the Windows UTF-8 code page for piped status line rendering.
54
+ - **📁 Git Root Dir widget (v2.0.27)** - Added a new Git widget that shows the repository root directory name.
55
+ - **🏷️ Session Name widget (v2.0.26)** - Added a new widget that shows the current Claude Code session name from `/rename`.
56
+ - **🏠 Current Working Directory home abbreviation (v2.0.26)** - Added a `~` abbreviation option for CWD display in both preview and live rendering.
57
+ - **🧠 Context model suffix fix (v2.0.26)** - Context widgets now recognize the `[1m]` suffix across models, not just a single model path.
58
+ - **🧭 Widget picker UX updates (v2.0.26)** - Improved widget discovery/navigation and added clearer, safer clear-line behavior.
59
+ - **⌨️ TUI editor input fix (v2.0.26)** - Prevented shortcut/input leakage into widget editor flows.
60
+ - **📄 Repo docs update (v2.0.26)** - Migrated guidance from `CLAUDE.md` to `AGENTS.md` (with symlink compatibility).
61
+
49
62
  ### v2.0.16 - Add fish style path abbreviation toggle to Current Working Directory widget
50
63
 
51
64
  ### v2.0.15 - Block Timer calculation fixes
@@ -51405,7 +51405,7 @@ import { execSync as execSync3 } from "child_process";
51405
51405
  import * as fs5 from "fs";
51406
51406
  import * as path4 from "path";
51407
51407
  var __dirname = "/Users/sirmalloc/Projects/Personal/ccstatusline/src/utils";
51408
- var PACKAGE_VERSION = "2.0.26";
51408
+ var PACKAGE_VERSION = "2.0.28";
51409
51409
  function getPackageVersion() {
51410
51410
  if (/^\d+\.\d+\.\d+/.test(PACKAGE_VERSION)) {
51411
51411
  return PACKAGE_VERSION;
@@ -52313,9 +52313,39 @@ class OutputStyleWidget {
52313
52313
  return true;
52314
52314
  }
52315
52315
  }
52316
- // src/widgets/GitBranch.ts
52316
+ // src/utils/git.ts
52317
52317
  import { execSync as execSync4 } from "child_process";
52318
+ function resolveGitCwd(context) {
52319
+ const candidates = [
52320
+ context.data?.cwd,
52321
+ context.data?.workspace?.current_dir,
52322
+ context.data?.workspace?.project_dir
52323
+ ];
52324
+ for (const candidate of candidates) {
52325
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
52326
+ return candidate;
52327
+ }
52328
+ }
52329
+ return;
52330
+ }
52331
+ function runGit(command, context) {
52332
+ try {
52333
+ const cwd2 = resolveGitCwd(context);
52334
+ const output = execSync4(`git ${command}`, {
52335
+ encoding: "utf8",
52336
+ stdio: ["pipe", "pipe", "ignore"],
52337
+ ...cwd2 ? { cwd: cwd2 } : {}
52338
+ }).trim();
52339
+ return output.length > 0 ? output : null;
52340
+ } catch {
52341
+ return null;
52342
+ }
52343
+ }
52344
+ function isInsideGitWorkTree(context) {
52345
+ return runGit("rev-parse --is-inside-work-tree", context) === "true";
52346
+ }
52318
52347
 
52348
+ // src/widgets/GitBranch.ts
52319
52349
  class GitBranchWidget {
52320
52350
  getDefaultColor() {
52321
52351
  return "magenta";
@@ -52358,21 +52388,16 @@ class GitBranchWidget {
52358
52388
  if (context.isPreview) {
52359
52389
  return item.rawValue ? "main" : "⎇ main";
52360
52390
  }
52361
- const branch = this.getGitBranch();
52391
+ if (!isInsideGitWorkTree(context)) {
52392
+ return hideNoGit ? null : "⎇ no git";
52393
+ }
52394
+ const branch = this.getGitBranch(context);
52362
52395
  if (branch)
52363
52396
  return item.rawValue ? branch : `⎇ ${branch}`;
52364
52397
  return hideNoGit ? null : "⎇ no git";
52365
52398
  }
52366
- getGitBranch() {
52367
- try {
52368
- const branch = execSync4("git branch --show-current", {
52369
- encoding: "utf8",
52370
- stdio: ["pipe", "pipe", "ignore"]
52371
- }).trim();
52372
- return branch || null;
52373
- } catch {
52374
- return null;
52375
- }
52399
+ getGitBranch(context) {
52400
+ return runGit("branch --show-current", context);
52376
52401
  }
52377
52402
  getCustomKeybinds() {
52378
52403
  return [
@@ -52387,8 +52412,6 @@ class GitBranchWidget {
52387
52412
  }
52388
52413
  }
52389
52414
  // src/widgets/GitChanges.ts
52390
- import { execSync as execSync5 } from "child_process";
52391
-
52392
52415
  class GitChangesWidget {
52393
52416
  getDefaultColor() {
52394
52417
  return "yellow";
@@ -52431,40 +52454,107 @@ class GitChangesWidget {
52431
52454
  if (context.isPreview) {
52432
52455
  return "(+42,-10)";
52433
52456
  }
52434
- const changes = this.getGitChanges();
52457
+ if (!isInsideGitWorkTree(context)) {
52458
+ return hideNoGit ? null : "(no git)";
52459
+ }
52460
+ const changes = this.getGitChanges(context);
52435
52461
  if (changes)
52436
52462
  return `(+${changes.insertions},-${changes.deletions})`;
52437
52463
  else
52438
52464
  return hideNoGit ? null : "(no git)";
52439
52465
  }
52440
- getGitChanges() {
52441
- try {
52442
- let totalInsertions = 0;
52443
- let totalDeletions = 0;
52444
- const unstagedStat = execSync5("git diff --shortstat", {
52445
- encoding: "utf8",
52446
- stdio: ["pipe", "pipe", "ignore"]
52447
- }).trim();
52448
- const stagedStat = execSync5("git diff --cached --shortstat", {
52449
- encoding: "utf8",
52450
- stdio: ["pipe", "pipe", "ignore"]
52451
- }).trim();
52452
- if (unstagedStat) {
52453
- const insertMatch = /(\d+) insertion/.exec(unstagedStat);
52454
- const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
52455
- totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
52456
- totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
52457
- }
52458
- if (stagedStat) {
52459
- const insertMatch = /(\d+) insertion/.exec(stagedStat);
52460
- const deleteMatch = /(\d+) deletion/.exec(stagedStat);
52461
- totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
52462
- totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
52463
- }
52464
- return { insertions: totalInsertions, deletions: totalDeletions };
52465
- } catch {
52466
- return null;
52466
+ getGitChanges(context) {
52467
+ let totalInsertions = 0;
52468
+ let totalDeletions = 0;
52469
+ const unstagedStat = runGit("diff --shortstat", context) ?? "";
52470
+ const stagedStat = runGit("diff --cached --shortstat", context) ?? "";
52471
+ if (unstagedStat) {
52472
+ const insertMatch = /(\d+) insertion/.exec(unstagedStat);
52473
+ const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
52474
+ totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
52475
+ totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
52476
+ }
52477
+ if (stagedStat) {
52478
+ const insertMatch = /(\d+) insertion/.exec(stagedStat);
52479
+ const deleteMatch = /(\d+) deletion/.exec(stagedStat);
52480
+ totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
52481
+ totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
52482
+ }
52483
+ return { insertions: totalInsertions, deletions: totalDeletions };
52484
+ }
52485
+ getCustomKeybinds() {
52486
+ return [
52487
+ { key: "h", label: "(h)ide 'no git' message", action: "toggle-nogit" }
52488
+ ];
52489
+ }
52490
+ supportsRawValue() {
52491
+ return false;
52492
+ }
52493
+ supportsColors(item) {
52494
+ return true;
52495
+ }
52496
+ }
52497
+ // src/widgets/GitRootDir.ts
52498
+ class GitRootDirWidget {
52499
+ getDefaultColor() {
52500
+ return "cyan";
52501
+ }
52502
+ getDescription() {
52503
+ return "Shows the git repository root directory name";
52504
+ }
52505
+ getDisplayName() {
52506
+ return "Git Root Dir";
52507
+ }
52508
+ getCategory() {
52509
+ return "Git";
52510
+ }
52511
+ getEditorDisplay(item) {
52512
+ const hideNoGit = item.metadata?.hideNoGit === "true";
52513
+ const modifiers = [];
52514
+ if (hideNoGit) {
52515
+ modifiers.push("hide 'no git'");
52467
52516
  }
52517
+ return {
52518
+ displayText: this.getDisplayName(),
52519
+ modifierText: modifiers.length > 0 ? `(${modifiers.join(", ")})` : undefined
52520
+ };
52521
+ }
52522
+ handleEditorAction(action, item) {
52523
+ if (action === "toggle-nogit") {
52524
+ const currentState = item.metadata?.hideNoGit === "true";
52525
+ return {
52526
+ ...item,
52527
+ metadata: {
52528
+ ...item.metadata,
52529
+ hideNoGit: (!currentState).toString()
52530
+ }
52531
+ };
52532
+ }
52533
+ return null;
52534
+ }
52535
+ render(item, context, _settings) {
52536
+ const hideNoGit = item.metadata?.hideNoGit === "true";
52537
+ if (context.isPreview) {
52538
+ return "my-repo";
52539
+ }
52540
+ if (!isInsideGitWorkTree(context)) {
52541
+ return hideNoGit ? null : "no git";
52542
+ }
52543
+ const rootDir = this.getGitRootDir(context);
52544
+ if (rootDir) {
52545
+ return this.getRootDirName(rootDir);
52546
+ }
52547
+ return hideNoGit ? null : "no git";
52548
+ }
52549
+ getGitRootDir(context) {
52550
+ return runGit("rev-parse --show-toplevel", context);
52551
+ }
52552
+ getRootDirName(rootDir) {
52553
+ const trimmedRootDir = rootDir.replace(/[\\/]+$/, "");
52554
+ const normalizedRootDir = trimmedRootDir.length > 0 ? trimmedRootDir : rootDir;
52555
+ const parts = normalizedRootDir.split(/[\\/]/).filter(Boolean);
52556
+ const lastPart = parts[parts.length - 1];
52557
+ return lastPart && lastPart.length > 0 ? lastPart : normalizedRootDir;
52468
52558
  }
52469
52559
  getCustomKeybinds() {
52470
52560
  return [
@@ -52479,8 +52569,6 @@ class GitChangesWidget {
52479
52569
  }
52480
52570
  }
52481
52571
  // src/widgets/GitWorktree.ts
52482
- import { execSync as execSync6 } from "child_process";
52483
-
52484
52572
  class GitWorktreeWidget {
52485
52573
  getDefaultColor() {
52486
52574
  return "blue";
@@ -52522,24 +52610,29 @@ class GitWorktreeWidget {
52522
52610
  const hideNoGit = item.metadata?.hideNoGit === "true";
52523
52611
  if (context.isPreview)
52524
52612
  return item.rawValue ? "main" : "\uD81A\uDC30 main";
52525
- const worktree = this.getGitWorktree();
52613
+ if (!isInsideGitWorkTree(context)) {
52614
+ return hideNoGit ? null : "\uD81A\uDC30 no git";
52615
+ }
52616
+ const worktree = this.getGitWorktree(context);
52526
52617
  if (worktree)
52527
52618
  return item.rawValue ? worktree : `\uD81A\uDC30 ${worktree}`;
52528
52619
  return hideNoGit ? null : "\uD81A\uDC30 no git";
52529
52620
  }
52530
- getGitWorktree() {
52531
- try {
52532
- const worktreeDir = execSync6("git rev-parse --git-dir", {
52533
- encoding: "utf8",
52534
- stdio: ["pipe", "pipe", "ignore"]
52535
- }).trim();
52536
- if (worktreeDir.endsWith("/.git") || worktreeDir === ".git")
52537
- return "main";
52538
- const [, worktree] = worktreeDir.split(".git/worktrees/");
52539
- return worktree ?? null;
52540
- } catch {
52621
+ getGitWorktree(context) {
52622
+ const worktreeDir = runGit("rev-parse --git-dir", context);
52623
+ if (!worktreeDir) {
52541
52624
  return null;
52542
52625
  }
52626
+ const normalizedGitDir = worktreeDir.replace(/\\/g, "/");
52627
+ if (normalizedGitDir.endsWith("/.git") || normalizedGitDir === ".git")
52628
+ return "main";
52629
+ const marker = ".git/worktrees/";
52630
+ const markerIndex = normalizedGitDir.lastIndexOf(marker);
52631
+ if (markerIndex === -1) {
52632
+ return null;
52633
+ }
52634
+ const worktree = normalizedGitDir.slice(markerIndex + marker.length);
52635
+ return worktree.length > 0 ? worktree : null;
52543
52636
  }
52544
52637
  getCustomKeybinds() {
52545
52638
  return [
@@ -53823,7 +53916,7 @@ var CustomTextEditor = ({ widget, onComplete, onCancel }) => {
53823
53916
  }, undefined, true, undefined, this);
53824
53917
  };
53825
53918
  // src/widgets/CustomCommand.tsx
53826
- import { execSync as execSync7 } from "child_process";
53919
+ import { execSync as execSync5 } from "child_process";
53827
53920
  var import_react30 = __toESM(require_react(), 1);
53828
53921
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
53829
53922
 
@@ -53872,7 +53965,7 @@ class CustomCommandWidget {
53872
53965
  try {
53873
53966
  const timeout = item.timeout ?? 1000;
53874
53967
  const jsonInput = JSON.stringify(context.data);
53875
- let output = execSync7(item.commandPath, {
53968
+ let output = execSync5(item.commandPath, {
53876
53969
  encoding: "utf8",
53877
53970
  input: jsonInput,
53878
53971
  timeout,
@@ -54533,6 +54626,7 @@ var widgetRegistry = new Map([
54533
54626
  ["output-style", new OutputStyleWidget],
54534
54627
  ["git-branch", new GitBranchWidget],
54535
54628
  ["git-changes", new GitChangesWidget],
54629
+ ["git-root-dir", new GitRootDirWidget],
54536
54630
  ["git-worktree", new GitWorktreeWidget],
54537
54631
  ["current-working-dir", new CurrentWorkingDirWidget],
54538
54632
  ["tokens-input", new TokensInputWidget],
@@ -56046,7 +56140,11 @@ var ItemsEditor = ({ widgets, onUpdate, onBack, lineNumber, settings }) => {
56046
56140
  }
56047
56141
  } else if (input === "r" && widgets.length > 0) {
56048
56142
  const currentWidget2 = widgets[selectedIndex];
56049
- if (currentWidget2 && currentWidget2.type !== "separator" && currentWidget2.type !== "flex-separator" && currentWidget2.type !== "custom-text") {
56143
+ if (currentWidget2 && currentWidget2.type !== "separator" && currentWidget2.type !== "flex-separator") {
56144
+ const widgetImpl = getWidget(currentWidget2.type);
56145
+ if (!widgetImpl?.supportsRawValue()) {
56146
+ return;
56147
+ }
56050
56148
  const newWidgets = [...widgets];
56051
56149
  newWidgets[selectedIndex] = { ...currentWidget2, rawValue: !currentWidget2.rawValue };
56052
56150
  onUpdate(newWidgets);
@@ -56455,6 +56553,7 @@ var ItemsEditor = ({ widgets, onUpdate, onBack, lineNumber, settings }) => {
56455
56553
  const isSelected = index === selectedIndex;
56456
56554
  const widgetImpl = widget.type !== "separator" && widget.type !== "flex-separator" ? getWidget(widget.type) : null;
56457
56555
  const { displayText, modifierText } = widgetImpl?.getEditorDisplay(widget) ?? { displayText: getWidgetDisplay(widget) };
56556
+ const supportsRawValue = widgetImpl?.supportsRawValue() ?? false;
56458
56557
  return /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Box_default, {
56459
56558
  flexDirection: "row",
56460
56559
  flexWrap: "nowrap",
@@ -56477,7 +56576,7 @@ var ItemsEditor = ({ widgets, onUpdate, onBack, lineNumber, settings }) => {
56477
56576
  modifierText
56478
56577
  ]
56479
56578
  }, undefined, true, undefined, this),
56480
- widget.rawValue && /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Text, {
56579
+ supportsRawValue && widget.rawValue && /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Text, {
56481
56580
  dimColor: true,
56482
56581
  children: " (raw value)"
56483
56582
  }, undefined, false, undefined, this),
@@ -58836,6 +58935,8 @@ var StatusJSONSchema = exports_external.looseObject({
58836
58935
 
58837
58936
  // src/utils/jsonl.ts
58838
58937
  import * as fs7 from "fs";
58938
+ import { createHash } from "node:crypto";
58939
+ import os7 from "node:os";
58839
58940
  import path6 from "node:path";
58840
58941
 
58841
58942
  // node_modules/tinyglobby/dist/index.mjs
@@ -59627,6 +59728,81 @@ import { promisify } from "util";
59627
59728
  var readFile4 = promisify(fs7.readFile);
59628
59729
  var readFileSync5 = fs7.readFileSync;
59629
59730
  var statSync4 = fs7.statSync;
59731
+ var writeFileSync2 = fs7.writeFileSync;
59732
+ var mkdirSync3 = fs7.mkdirSync;
59733
+ var existsSync7 = fs7.existsSync;
59734
+ function normalizeConfigDir(configDir) {
59735
+ return path6.resolve(configDir);
59736
+ }
59737
+ function getBlockCachePath(configDir = getClaudeConfigDir()) {
59738
+ const normalizedConfigDir = normalizeConfigDir(configDir);
59739
+ const configHash = createHash("sha256").update(normalizedConfigDir).digest("hex").slice(0, 16);
59740
+ return path6.join(os7.homedir(), ".cache", "ccstatusline", `block-cache-${configHash}.json`);
59741
+ }
59742
+ function readBlockCache(expectedConfigDir) {
59743
+ try {
59744
+ const normalizedExpectedConfigDir = expectedConfigDir !== undefined ? normalizeConfigDir(expectedConfigDir) : undefined;
59745
+ const cachePath = getBlockCachePath(normalizedExpectedConfigDir);
59746
+ if (!existsSync7(cachePath)) {
59747
+ return null;
59748
+ }
59749
+ const content = readFileSync5(cachePath, "utf-8");
59750
+ const cache3 = JSON.parse(content);
59751
+ if (typeof cache3.startTime !== "string") {
59752
+ return null;
59753
+ }
59754
+ if (normalizedExpectedConfigDir !== undefined) {
59755
+ if (typeof cache3.configDir !== "string") {
59756
+ return null;
59757
+ }
59758
+ if (cache3.configDir !== normalizedExpectedConfigDir) {
59759
+ return null;
59760
+ }
59761
+ }
59762
+ const date5 = new Date(cache3.startTime);
59763
+ if (Number.isNaN(date5.getTime())) {
59764
+ return null;
59765
+ }
59766
+ return date5;
59767
+ } catch {
59768
+ return null;
59769
+ }
59770
+ }
59771
+ function writeBlockCache(startTime, configDir = getClaudeConfigDir()) {
59772
+ try {
59773
+ const normalizedConfigDir = normalizeConfigDir(configDir);
59774
+ const cachePath = getBlockCachePath(normalizedConfigDir);
59775
+ const cacheDir = path6.dirname(cachePath);
59776
+ if (!existsSync7(cacheDir)) {
59777
+ mkdirSync3(cacheDir, { recursive: true });
59778
+ }
59779
+ const cache3 = {
59780
+ startTime: startTime.toISOString(),
59781
+ configDir: normalizedConfigDir
59782
+ };
59783
+ writeFileSync2(cachePath, JSON.stringify(cache3), "utf-8");
59784
+ } catch {}
59785
+ }
59786
+ function getCachedBlockMetrics(sessionDurationHours = 5) {
59787
+ const sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
59788
+ const now = new Date;
59789
+ const activeConfigDir = getClaudeConfigDir();
59790
+ const cachedStartTime = readBlockCache(activeConfigDir);
59791
+ if (cachedStartTime) {
59792
+ const blockEndTime = new Date(cachedStartTime.getTime() + sessionDurationMs);
59793
+ if (now.getTime() <= blockEndTime.getTime()) {
59794
+ return {
59795
+ startTime: cachedStartTime,
59796
+ lastActivity: now
59797
+ };
59798
+ }
59799
+ }
59800
+ const metrics = getBlockMetrics();
59801
+ if (metrics) {
59802
+ writeBlockCache(metrics.startTime, activeConfigDir);
59803
+ }
59804
+ return metrics;
59805
+ }
59630
59806
  async function getSessionDuration(transcriptPath) {
59631
59807
  try {
59632
59808
  if (!fs7.existsSync(transcriptPath)) {
@@ -59887,6 +60063,15 @@ async function readStdin() {
59887
60063
  return null;
59888
60064
  }
59889
60065
  }
60066
+ async function ensureWindowsUtf8CodePage() {
60067
+ if (process.platform !== "win32") {
60068
+ return;
60069
+ }
60070
+ try {
60071
+ const { execFileSync } = await import("child_process");
60072
+ execFileSync("chcp.com", ["65001"], { stdio: "ignore" });
60073
+ } catch {}
60074
+ }
59890
60075
  async function renderMultipleLines(data) {
59891
60076
  const settings = await loadSettings();
59892
60077
  source_default.level = settings.colorLevel;
@@ -59905,7 +60090,7 @@ async function renderMultipleLines(data) {
59905
60090
  }
59906
60091
  let blockMetrics = null;
59907
60092
  if (hasBlockTimer) {
59908
- blockMetrics = getBlockMetrics();
60093
+ blockMetrics = getCachedBlockMetrics();
59909
60094
  }
59910
60095
  const context = {
59911
60096
  data,
@@ -59953,6 +60138,7 @@ async function renderMultipleLines(data) {
59953
60138
  }
59954
60139
  async function main() {
59955
60140
  if (!process.stdin.isTTY) {
60141
+ await ensureWindowsUtf8CodePage();
59956
60142
  const input = await readStdin();
59957
60143
  if (input && input.trim() !== "") {
59958
60144
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstatusline",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
4
4
  "description": "A customizable status line formatter for Claude Code CLI",
5
5
  "module": "src/ccstatusline.ts",
6
6
  "type": "module",
@@ -70,4 +70,4 @@
70
70
  "patchedDependencies": {
71
71
  "ink@6.2.0": "patches/ink@6.2.0.patch"
72
72
  }
73
- }
73
+ }