@say8425/cc-statusline 4.0.0 → 5.0.0

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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/index.js +194 -62
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -69,8 +69,8 @@ Add the following to `~/.claude/settings.json`:
69
69
 
70
70
  | Emoji | Description |
71
71
  | ----- | ------------------------ |
72
- | 📁 | Project folder name |
73
- | 🌲 | Worktree name (shown in worktree sessions) |
72
+ | 📁 | Project folder name (click to open in file manager) |
73
+ | 🌲 | Worktree name (click to open worktree folder) |
74
74
  | 🌿 | Current Git branch |
75
75
  | ⏱️ | Session elapsed time |
76
76
  | 💰 | Session cost in USD |
@@ -81,7 +81,7 @@ Add the following to `~/.claude/settings.json`:
81
81
  | ⏰ | Weekly limit reset time |
82
82
  | 📅 | 7-day utilization % |
83
83
  | ✏️ | Uncommitted changes (click to open diff viewer) |
84
- | 📎 | Pull request link |
84
+ | 📎 | Pull request link — color-coded state in brackets (`[Open]`/`[Draft]`/`[Merged]`/`[Closed]`) immediately followed by a color-coded CI check summary in parens (`(N passed)`/`(N running)`/`(N failed)`) when checks exist |
85
85
 
86
86
  ## Diff Viewer
87
87
 
@@ -101,10 +101,11 @@ Click `✏️` in the statusline to open a local diff viewer in your browser. Th
101
101
  ![image_diff](docs/image_diff.png)
102
102
  - **Unified / Split** view toggle
103
103
  - **Watch mode**: auto-refresh (~2s polling) that detects changes while preserving scroll position
104
- - **File tree**: left/right placement and flatten (collapse empty directories) toggles
104
+ - **File tree**: left/right placement, drag-resizable width, flatten (collapse empty directories), and hide-sidebar toggles
105
105
  - **File folding**: click any file header to collapse/expand; lockfiles and files with more than 1,500 changed lines start collapsed
106
106
  - **In-app search** (`Cmd/Ctrl+F`): searches the entire diff including deleted lines, with match navigation and highlighting
107
107
  - **Copy path**: hover a file header to copy the file's relative path
108
+ - **diff-grab**: select lines in the diff (drag the text, or use the gutter `+`), type a prompt, and press Enter — the file path, line range, code snippet, and your prompt are copied to the clipboard, ready to paste into an agent like Claude Code
108
109
  - **Include untracked** files toggle
109
110
 
110
111
  ### How It Works (Diff Viewer)
package/dist/index.js CHANGED
@@ -181,6 +181,96 @@ var buildDiffViewerUrl = (params) => {
181
181
  return `http://127.0.0.1:${params.port}/?${query.toString()}`;
182
182
  };
183
183
 
184
+ // src/format/formatNumber.ts
185
+ var formatNumber = (n) => n.toLocaleString("en-US");
186
+ // src/format/formatResetDate.ts
187
+ var formatResetDate = (date) => {
188
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
189
+ const day = date.getDate().toString().padStart(2, "0");
190
+ const hours = date.getHours().toString().padStart(2, "0");
191
+ const minutes = date.getMinutes().toString().padStart(2, "0");
192
+ return `${month}/${day} ${hours}:${minutes}`;
193
+ };
194
+ // src/format/formatTime.ts
195
+ var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
196
+ // src/colors.ts
197
+ var C = {
198
+ RESET: "\x1B[0m",
199
+ CYAN: "\x1B[38;2;0;255;255m",
200
+ MAGENTA: "\x1B[38;2;255;100;200m",
201
+ GREEN: "\x1B[38;2;100;255;100m",
202
+ YELLOW: "\x1B[38;2;255;220;100m",
203
+ RED: "\x1B[38;2;255;100;100m",
204
+ BLUE: "\x1B[38;2;100;150;255m",
205
+ WHITE: "\x1B[38;2;200;200;200m",
206
+ GREEN_MUTED: "\x1B[38;2;140;195;140m",
207
+ YELLOW_MUTED: "\x1B[38;2;210;185;130m",
208
+ RED_MUTED: "\x1B[38;2;210;130;130m",
209
+ MAGENTA_MUTED: "\x1B[38;2;195;140;175m",
210
+ UNDERLINE: "\x1B[4m"
211
+ };
212
+ var getUsageColor = (pct) => {
213
+ if (pct < 50)
214
+ return C.WHITE;
215
+ if (pct < 80)
216
+ return C.YELLOW;
217
+ return C.RED;
218
+ };
219
+
220
+ // src/format/prStatus.ts
221
+ var prStateText = (state, isDraft) => {
222
+ if (isDraft)
223
+ return "Draft";
224
+ if (state === "OPEN")
225
+ return "Open";
226
+ if (state === "MERGED")
227
+ return "Merged";
228
+ return "Closed";
229
+ };
230
+ var prStateColor = (state, isDraft) => {
231
+ if (isDraft)
232
+ return C.WHITE;
233
+ if (state === "OPEN")
234
+ return C.GREEN_MUTED;
235
+ if (state === "MERGED")
236
+ return C.MAGENTA_MUTED;
237
+ return C.RED_MUTED;
238
+ };
239
+ var ciSummaryText = (ci) => {
240
+ if (ci === null)
241
+ return "";
242
+ if (ci.conclusion === "failure")
243
+ return `${ci.count} failed`;
244
+ if (ci.conclusion === "pending")
245
+ return `${ci.count} running`;
246
+ return `${ci.count} passed`;
247
+ };
248
+ var ciSummaryColor = (ci) => {
249
+ if (ci === null)
250
+ return "";
251
+ if (ci.conclusion === "failure")
252
+ return C.RED_MUTED;
253
+ if (ci.conclusion === "pending")
254
+ return C.YELLOW_MUTED;
255
+ return C.GREEN_MUTED;
256
+ };
257
+ // src/format/toFileUrl.ts
258
+ var toFileUrl = (absolutePath) => {
259
+ const normalized = absolutePath.replace(/\\/g, "/");
260
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
261
+ try {
262
+ return `file://${encodeURI(withLeadingSlash)}`;
263
+ } catch {
264
+ const safe = Array.from(withLeadingSlash).map((ch) => {
265
+ try {
266
+ return encodeURI(ch);
267
+ } catch {
268
+ return "%EF%BF%BD";
269
+ }
270
+ }).join("");
271
+ return `file://${safe}`;
272
+ }
273
+ };
184
274
  // src/git/baseChanges.ts
185
275
  var {$: $2 } = globalThis.Bun;
186
276
 
@@ -188,8 +278,11 @@ var {$: $2 } = globalThis.Bun;
188
278
  var cache = {
189
279
  branch: { value: "", timestamp: 0 },
190
280
  gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
191
- prUrl: { value: null, timestamp: 0 },
192
- mainProjectName: { value: null, timestamp: 0 },
281
+ prInfo: { value: null, timestamp: 0 },
282
+ mainProject: {
283
+ value: null,
284
+ timestamp: 0
285
+ },
193
286
  baseRef: {
194
287
  value: null,
195
288
  timestamp: 0
@@ -199,8 +292,8 @@ var cache = {
199
292
  var CACHE_TTL = {
200
293
  branch: 5000,
201
294
  gitChanges: 0,
202
- prUrl: 30000,
203
- mainProjectName: 300000,
295
+ prInfo: 30000,
296
+ mainProject: 300000,
204
297
  baseRef: 30000,
205
298
  ultracode: 5000
206
299
  };
@@ -308,71 +401,91 @@ ${staged}`);
308
401
  };
309
402
  // src/git/pr.ts
310
403
  var {$: $5 } = globalThis.Bun;
311
- var getPrUrlCached = async () => {
312
- if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
313
- return cache.prUrl.value;
404
+
405
+ // src/git/ciStatus.ts
406
+ var FAILURE_CONCLUSIONS = new Set([
407
+ "FAILURE",
408
+ "TIMED_OUT",
409
+ "CANCELLED",
410
+ "ACTION_REQUIRED",
411
+ "STALE"
412
+ ]);
413
+ var PASS_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
414
+ var FAILURE_STATES = new Set(["FAILURE", "ERROR"]);
415
+ var aggregateCiStatus = (checks) => {
416
+ if (checks.length === 0)
417
+ return null;
418
+ let failedCount = 0;
419
+ let pendingCount = 0;
420
+ let passedCount = 0;
421
+ for (const check of checks) {
422
+ if (check.conclusion != null) {
423
+ if (FAILURE_CONCLUSIONS.has(check.conclusion))
424
+ failedCount++;
425
+ else if (PASS_CONCLUSIONS.has(check.conclusion))
426
+ passedCount++;
427
+ else
428
+ pendingCount++;
429
+ } else if (check.status != null && check.status !== "COMPLETED") {
430
+ pendingCount++;
431
+ } else if (check.state != null) {
432
+ if (FAILURE_STATES.has(check.state))
433
+ failedCount++;
434
+ else if (check.state === "SUCCESS")
435
+ passedCount++;
436
+ else
437
+ pendingCount++;
438
+ }
439
+ }
440
+ if (failedCount > 0)
441
+ return { conclusion: "failure", count: failedCount };
442
+ if (pendingCount > 0)
443
+ return { conclusion: "pending", count: pendingCount };
444
+ return { conclusion: "success", count: passedCount };
445
+ };
446
+
447
+ // src/git/pr.ts
448
+ var getPrInfoCached = async () => {
449
+ if (Date.now() - cache.prInfo.timestamp < CACHE_TTL.prInfo) {
450
+ return cache.prInfo.value;
314
451
  }
315
452
  try {
316
- const result = await $5`gh pr view --json url -q .url 2>/dev/null`.text();
317
- cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
318
- return cache.prUrl.value;
453
+ const result = await $5`gh pr view --json url,state,isDraft,statusCheckRollup 2>/dev/null`.text();
454
+ const parsed = JSON.parse(result);
455
+ const value = {
456
+ url: parsed.url,
457
+ state: parsed.state,
458
+ isDraft: parsed.isDraft,
459
+ ciStatus: aggregateCiStatus(parsed.statusCheckRollup ?? [])
460
+ };
461
+ cache.prInfo = { value, timestamp: Date.now() };
462
+ return value;
319
463
  } catch {
320
- return cache.prUrl.value;
464
+ return cache.prInfo.value;
321
465
  }
322
466
  };
323
467
  // src/git/worktree.ts
324
468
  var {$: $6 } = globalThis.Bun;
325
- var getMainProjectNameCached = async () => {
326
- if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
327
- return cache.mainProjectName.value;
469
+ var getMainProjectCached = async () => {
470
+ if (Date.now() - cache.mainProject.timestamp < CACHE_TTL.mainProject) {
471
+ return cache.mainProject.value;
328
472
  }
329
473
  try {
330
474
  const result = await $6`git rev-parse --git-common-dir 2>/dev/null`.text();
331
475
  const gitCommonDir = result.trim();
332
476
  if (gitCommonDir === ".git") {
333
- cache.mainProjectName = { value: null, timestamp: Date.now() };
477
+ cache.mainProject = { value: null, timestamp: Date.now() };
334
478
  return null;
335
479
  }
336
480
  const projectRoot = gitCommonDir.replace(/\/.git$/, "");
337
- const projectName = projectRoot.split("/").pop() || null;
338
- cache.mainProjectName = { value: projectName, timestamp: Date.now() };
339
- return projectName;
481
+ const projectName = projectRoot.split("/").pop();
482
+ const value = projectName ? { name: projectName, path: projectRoot } : null;
483
+ cache.mainProject = { value, timestamp: Date.now() };
484
+ return value;
340
485
  } catch {
341
- return cache.mainProjectName.value;
486
+ return cache.mainProject.value;
342
487
  }
343
488
  };
344
- // src/colors.ts
345
- var C = {
346
- RESET: "\x1B[0m",
347
- CYAN: "\x1B[38;2;0;255;255m",
348
- MAGENTA: "\x1B[38;2;255;100;200m",
349
- GREEN: "\x1B[38;2;100;255;100m",
350
- YELLOW: "\x1B[38;2;255;220;100m",
351
- RED: "\x1B[38;2;255;100;100m",
352
- BLUE: "\x1B[38;2;100;150;255m",
353
- WHITE: "\x1B[38;2;200;200;200m",
354
- UNDERLINE: "\x1B[4m"
355
- };
356
- var getUsageColor = (pct) => {
357
- if (pct < 50)
358
- return C.WHITE;
359
- if (pct < 80)
360
- return C.YELLOW;
361
- return C.RED;
362
- };
363
-
364
- // src/format/formatNumber.ts
365
- var formatNumber = (n) => n.toLocaleString("en-US");
366
- // src/format/formatResetDate.ts
367
- var formatResetDate = (date) => {
368
- const month = (date.getMonth() + 1).toString().padStart(2, "0");
369
- const day = date.getDate().toString().padStart(2, "0");
370
- const hours = date.getHours().toString().padStart(2, "0");
371
- const minutes = date.getMinutes().toString().padStart(2, "0");
372
- return `${month}/${day} ${hours}:${minutes}`;
373
- };
374
- // src/format/formatTime.ts
375
- var formatTime = (hours, mins) => `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
376
489
  // src/render.ts
377
490
  var renderStatusLine = (ctx) => {
378
491
  const lines = [];
@@ -384,9 +497,17 @@ var renderStatusLine = (ctx) => {
384
497
  const costUsd = ctx.claudeJson.cost?.total_cost_usd || 0;
385
498
  let line1;
386
499
  if (ctx.mainProjectName) {
387
- line1 = `${C.WHITE}\uD83D\uDCC1 ${ctx.mainProjectName}${C.RESET} | ${C.WHITE}\uD83C\uDF32 ${folder}${C.RESET}`;
500
+ const mainU = ctx.mainProjectUrl ? C.UNDERLINE : "";
501
+ const mainText = `${C.WHITE}\uD83D\uDCC1 ${mainU}${ctx.mainProjectName}${C.RESET}`;
502
+ const mainSeg = ctx.mainProjectUrl ? `\x1B]8;;${ctx.mainProjectUrl}\x07${mainText}\x1B]8;;\x07` : mainText;
503
+ const folderU = ctx.projectDirUrl ? C.UNDERLINE : "";
504
+ const folderText = `${C.WHITE}\uD83C\uDF32 ${folderU}${folder}${C.RESET}`;
505
+ const folderSeg = ctx.projectDirUrl ? `\x1B]8;;${ctx.projectDirUrl}\x07${folderText}\x1B]8;;\x07` : folderText;
506
+ line1 = `${mainSeg} | ${folderSeg}`;
388
507
  } else {
389
- line1 = `${C.WHITE}\uD83D\uDCC1 ${folder}${C.RESET}`;
508
+ const folderU = ctx.projectDirUrl ? C.UNDERLINE : "";
509
+ const folderText = `${C.WHITE}\uD83D\uDCC1 ${folderU}${folder}${C.RESET}`;
510
+ line1 = ctx.projectDirUrl ? `\x1B]8;;${ctx.projectDirUrl}\x07${folderText}\x1B]8;;\x07` : folderText;
390
511
  }
391
512
  if (ctx.branch) {
392
513
  line1 += ` | ${C.WHITE}\uD83C\uDF3F ${ctx.branch}${C.RESET}`;
@@ -437,7 +558,7 @@ var renderStatusLine = (ctx) => {
437
558
  }
438
559
  }
439
560
  const hasGitChanges = ctx.gitChanges.files > 0 || ctx.gitChanges.insertions > 0 || ctx.gitChanges.deletions > 0;
440
- if (hasGitChanges || ctx.baseChanges || ctx.prUrl) {
561
+ if (hasGitChanges || ctx.baseChanges || ctx.prInfo) {
441
562
  let line4 = "";
442
563
  if (hasGitChanges) {
443
564
  const u = ctx.diffViewerUrl ? C.UNDERLINE : "";
@@ -449,11 +570,17 @@ var renderStatusLine = (ctx) => {
449
570
  const baseText = `\u270F\uFE0F ${u}vs ${bc.base} ${C.WHITE}${bc.files} files${C.RESET}` + `${u} ${C.GREEN}+${bc.insertions}${C.RESET}` + `${u} ${C.RED}-${bc.deletions}${C.RESET}`;
450
571
  line4 += ctx.baseDiffViewerUrl ? `\x1B]8;;${ctx.baseDiffViewerUrl}\x07${baseText}\x1B]8;;\x07` : baseText;
451
572
  }
452
- if (ctx.prUrl) {
453
- const prLabel = ctx.prUrl.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
573
+ if (ctx.prInfo) {
574
+ const { url, state, isDraft, ciStatus } = ctx.prInfo;
575
+ const prLabel = url.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
576
+ const stateText = prStateText(state, isDraft);
577
+ const stateColor = prStateColor(state, isDraft);
578
+ const ciText = ciSummaryText(ciStatus);
579
+ const ciColor = ciSummaryColor(ciStatus);
580
+ const ciSuffix = ciText ? `${ciColor}${C.UNDERLINE}(${ciText})` : "";
454
581
  if (line4)
455
582
  line4 += " | ";
456
- line4 += `\uD83D\uDCCE ${C.WHITE}${C.UNDERLINE}\x1B]8;;${ctx.prUrl}\x07${prLabel}\x1B]8;;\x07${C.RESET}`;
583
+ line4 += `\uD83D\uDCCE ${C.WHITE}${C.UNDERLINE}\x1B]8;;${url}\x07${prLabel} ` + `${stateColor}${C.UNDERLINE}[${stateText}]${ciSuffix}${C.RESET}\x1B]8;;\x07`;
457
584
  }
458
585
  lines.push(line4);
459
586
  }
@@ -509,13 +636,16 @@ var getUltracodeCached = async (projectDir, homeDir = homedir2()) => {
509
636
  var main = async () => {
510
637
  const claudeJson = JSON.parse(await readStdin() || "{}");
511
638
  const repo = claudeJson.workspace?.project_dir || claudeJson.workspace?.current_dir || "";
512
- const [branch, gitChanges, prUrl, mainProjectName, ultracode] = await Promise.all([
639
+ const [branch, gitChanges, prInfo, mainProject, ultracode] = await Promise.all([
513
640
  getBranchCached(),
514
641
  getGitChangesCached(),
515
- getPrUrlCached(),
516
- getMainProjectNameCached(),
642
+ getPrInfoCached(),
643
+ getMainProjectCached(),
517
644
  getUltracodeCached(repo)
518
645
  ]);
646
+ const mainProjectName = mainProject?.name ?? null;
647
+ const projectDirUrl = repo ? toFileUrl(repo) : null;
648
+ const mainProjectUrl = mainProject ? toFileUrl(mainProject.path) : null;
519
649
  const hasChanges = gitChanges.files > 0 || gitChanges.insertions > 0 || gitChanges.deletions > 0;
520
650
  let diffViewerUrl = null;
521
651
  let baseDiffViewerUrl = null;
@@ -547,13 +677,15 @@ var main = async () => {
547
677
  claudeJson,
548
678
  branch,
549
679
  gitChanges,
550
- prUrl,
680
+ prInfo,
551
681
  ultracode,
552
682
  rateLimits: claudeJson.rate_limits ?? null,
553
683
  mainProjectName,
554
684
  diffViewerUrl,
555
685
  baseChanges,
556
- baseDiffViewerUrl
686
+ baseDiffViewerUrl,
687
+ projectDirUrl,
688
+ mainProjectUrl
557
689
  });
558
690
  for (const line of lines) {
559
691
  console.log(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@say8425/cc-statusline",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Custom statusline for Claude Code",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,13 +27,13 @@
27
27
  "typecheck": "tsc --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@say8425/diffdeck": "^0.2.2"
30
+ "@say8425/diffdeck": "^1.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.3.14",
34
- "oxfmt": "^0.57.0",
35
- "oxlint": "^1.72.0",
36
- "oxlint-tsgolint": "^0.24.0",
34
+ "oxfmt": "^0.60.0",
35
+ "oxlint": "^1.76.0",
36
+ "oxlint-tsgolint": "^7.0.2001",
37
37
  "typescript": "^6.0.3"
38
38
  }
39
39
  }