ccstatusline 2.0.27 → 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,18 +46,18 @@
46
46
 
47
47
  ## 🆕 Recent Updates
48
48
 
49
- ### v2.0.27 - Git Root Dir widget and raw-mode editor guardrails
50
-
51
- - **📁 Git Root Dir widget** - Added a new Git widget that shows the repository root directory name.
52
-
53
- ### v2.0.26 - Session naming, CWD options, and TUI workflow improvements
54
-
55
- - **🏷️ Session Name widget** - Added a new widget that shows the current Claude Code session name from `/rename`.
56
- - **🏠 Current Working Directory home abbreviation** - Added a `~` abbreviation option for CWD display in both preview and live rendering.
57
- - **🧠 Context model suffix fix** - Context widgets now recognize the `[1m]` suffix across models, not just a single model path.
58
- - **🧭 Widget picker UX updates** - Improved widget discovery/navigation and added clearer, safer clear-line behavior.
59
- - **⌨️ TUI editor input fix** - Prevented shortcut/input leakage into widget editor flows.
60
- - **📄 Repo docs update** - Migrated guidance from `CLAUDE.md` to `AGENTS.md` (with symlink compatibility).
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
61
 
62
62
  ### v2.0.16 - Add fish style path abbreviation toggle to Current Working Directory widget
63
63
 
@@ -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.27";
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,33 @@ 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;
52467
- }
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 };
52468
52484
  }
52469
52485
  getCustomKeybinds() {
52470
52486
  return [
@@ -52479,8 +52495,6 @@ class GitChangesWidget {
52479
52495
  }
52480
52496
  }
52481
52497
  // src/widgets/GitRootDir.ts
52482
- import { execSync as execSync6 } from "child_process";
52483
-
52484
52498
  class GitRootDirWidget {
52485
52499
  getDefaultColor() {
52486
52500
  return "cyan";
@@ -52523,22 +52537,17 @@ class GitRootDirWidget {
52523
52537
  if (context.isPreview) {
52524
52538
  return "my-repo";
52525
52539
  }
52526
- const rootDir = this.getGitRootDir();
52540
+ if (!isInsideGitWorkTree(context)) {
52541
+ return hideNoGit ? null : "no git";
52542
+ }
52543
+ const rootDir = this.getGitRootDir(context);
52527
52544
  if (rootDir) {
52528
52545
  return this.getRootDirName(rootDir);
52529
52546
  }
52530
52547
  return hideNoGit ? null : "no git";
52531
52548
  }
52532
- getGitRootDir() {
52533
- try {
52534
- const rootDir = execSync6("git rev-parse --show-toplevel", {
52535
- encoding: "utf8",
52536
- stdio: ["pipe", "pipe", "ignore"]
52537
- }).trim();
52538
- return rootDir || null;
52539
- } catch {
52540
- return null;
52541
- }
52549
+ getGitRootDir(context) {
52550
+ return runGit("rev-parse --show-toplevel", context);
52542
52551
  }
52543
52552
  getRootDirName(rootDir) {
52544
52553
  const trimmedRootDir = rootDir.replace(/[\\/]+$/, "");
@@ -52560,8 +52569,6 @@ class GitRootDirWidget {
52560
52569
  }
52561
52570
  }
52562
52571
  // src/widgets/GitWorktree.ts
52563
- import { execSync as execSync7 } from "child_process";
52564
-
52565
52572
  class GitWorktreeWidget {
52566
52573
  getDefaultColor() {
52567
52574
  return "blue";
@@ -52603,24 +52610,29 @@ class GitWorktreeWidget {
52603
52610
  const hideNoGit = item.metadata?.hideNoGit === "true";
52604
52611
  if (context.isPreview)
52605
52612
  return item.rawValue ? "main" : "\uD81A\uDC30 main";
52606
- const worktree = this.getGitWorktree();
52613
+ if (!isInsideGitWorkTree(context)) {
52614
+ return hideNoGit ? null : "\uD81A\uDC30 no git";
52615
+ }
52616
+ const worktree = this.getGitWorktree(context);
52607
52617
  if (worktree)
52608
52618
  return item.rawValue ? worktree : `\uD81A\uDC30 ${worktree}`;
52609
52619
  return hideNoGit ? null : "\uD81A\uDC30 no git";
52610
52620
  }
52611
- getGitWorktree() {
52612
- try {
52613
- const worktreeDir = execSync7("git rev-parse --git-dir", {
52614
- encoding: "utf8",
52615
- stdio: ["pipe", "pipe", "ignore"]
52616
- }).trim();
52617
- if (worktreeDir.endsWith("/.git") || worktreeDir === ".git")
52618
- return "main";
52619
- const [, worktree] = worktreeDir.split(".git/worktrees/");
52620
- return worktree ?? null;
52621
- } catch {
52621
+ getGitWorktree(context) {
52622
+ const worktreeDir = runGit("rev-parse --git-dir", context);
52623
+ if (!worktreeDir) {
52622
52624
  return null;
52623
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;
52624
52636
  }
52625
52637
  getCustomKeybinds() {
52626
52638
  return [
@@ -53904,7 +53916,7 @@ var CustomTextEditor = ({ widget, onComplete, onCancel }) => {
53904
53916
  }, undefined, true, undefined, this);
53905
53917
  };
53906
53918
  // src/widgets/CustomCommand.tsx
53907
- import { execSync as execSync8 } from "child_process";
53919
+ import { execSync as execSync5 } from "child_process";
53908
53920
  var import_react30 = __toESM(require_react(), 1);
53909
53921
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
53910
53922
 
@@ -53953,7 +53965,7 @@ class CustomCommandWidget {
53953
53965
  try {
53954
53966
  const timeout = item.timeout ?? 1000;
53955
53967
  const jsonInput = JSON.stringify(context.data);
53956
- let output = execSync8(item.commandPath, {
53968
+ let output = execSync5(item.commandPath, {
53957
53969
  encoding: "utf8",
53958
53970
  input: jsonInput,
53959
53971
  timeout,
@@ -58923,6 +58935,8 @@ var StatusJSONSchema = exports_external.looseObject({
58923
58935
 
58924
58936
  // src/utils/jsonl.ts
58925
58937
  import * as fs7 from "fs";
58938
+ import { createHash } from "node:crypto";
58939
+ import os7 from "node:os";
58926
58940
  import path6 from "node:path";
58927
58941
 
58928
58942
  // node_modules/tinyglobby/dist/index.mjs
@@ -59714,6 +59728,81 @@ import { promisify } from "util";
59714
59728
  var readFile4 = promisify(fs7.readFile);
59715
59729
  var readFileSync5 = fs7.readFileSync;
59716
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
+ }
59717
59806
  async function getSessionDuration(transcriptPath) {
59718
59807
  try {
59719
59808
  if (!fs7.existsSync(transcriptPath)) {
@@ -59974,6 +60063,15 @@ async function readStdin() {
59974
60063
  return null;
59975
60064
  }
59976
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
+ }
59977
60075
  async function renderMultipleLines(data) {
59978
60076
  const settings = await loadSettings();
59979
60077
  source_default.level = settings.colorLevel;
@@ -59992,7 +60090,7 @@ async function renderMultipleLines(data) {
59992
60090
  }
59993
60091
  let blockMetrics = null;
59994
60092
  if (hasBlockTimer) {
59995
- blockMetrics = getBlockMetrics();
60093
+ blockMetrics = getCachedBlockMetrics();
59996
60094
  }
59997
60095
  const context = {
59998
60096
  data,
@@ -60040,6 +60138,7 @@ async function renderMultipleLines(data) {
60040
60138
  }
60041
60139
  async function main() {
60042
60140
  if (!process.stdin.isTTY) {
60141
+ await ensureWindowsUtf8CodePage();
60043
60142
  const input = await readStdin();
60044
60143
  if (input && input.trim() !== "") {
60045
60144
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstatusline",
3
- "version": "2.0.27",
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",