Fix build issue with patching Ink, bump version to 1.0.15

This reverts commit 256b562cca.
This commit is contained in:
Matthew Breedlove
2025-08-12 03:08:18 -04:00
parent 256b562cca
commit e0012bc0eb
9 changed files with 1130 additions and 1778 deletions
+141 -3
View File
@@ -4,15 +4,15 @@
"": {
"name": "ccstatusline",
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1",
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.1.9",
"patch-package": "^8.0.0",
},
"peerDependencies": {
"typescript": "^5",
@@ -28,18 +28,36 @@
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
"ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
@@ -48,46 +66,136 @@
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-toolkit": ["es-toolkit@1.39.8", "", {}, "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ink": ["ink@6.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-YQ+lbMD79y3FBAJXXZnuRajLEgaMFp102361eY5NrBIEVCi9oFo7gNZU4z2LBWlcjZFiTt7jetlkIbKCCH4KJA=="],
"ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="],
"ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
@@ -96,12 +204,24 @@
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
"slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
@@ -110,6 +230,12 @@
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
@@ -118,20 +244,32 @@
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
"patch-package/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="],
"patch-package/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
}
}
-619
View File
@@ -1,619 +0,0 @@
{
"name": "ccstatusline",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ccstatusline",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1"
},
"bin": {
"ccstatusline": "dist/ccstatusline.js"
},
"devDependencies": {
"@types/bun": "latest"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"typescript": "^5"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
"version": "0.1.3",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@types/bun": {
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz",
"integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.2.19"
}
},
"node_modules/@types/node": {
"version": "24.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/@types/react": {
"version": "19.1.9",
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/ansi-escapes": {
"version": "7.0.0",
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/auto-bind": {
"version": "5.0.1",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bun-types": {
"version": "1.2.19",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
},
"peerDependencies": {
"@types/react": "^19"
}
},
"node_modules/chalk": {
"version": "5.5.0",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-boxes": {
"version": "3.0.0",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"restore-cursor": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/slice-ansi": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/code-excerpt": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"convert-to-spaces": "^2.0.1"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/convert-to-spaces": {
"version": "2.0.1",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.4.0",
"license": "MIT"
},
"node_modules/environment": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es-toolkit": {
"version": "1.39.8",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/figures": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/indent-string": {
"version": "5.0.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.1.3",
"ansi-escapes": "^7.0.0",
"ansi-styles": "^6.2.1",
"auto-bind": "^5.0.1",
"chalk": "^5.3.0",
"cli-boxes": "^3.0.0",
"cli-cursor": "^4.0.0",
"cli-truncate": "^4.0.0",
"code-excerpt": "^4.0.0",
"es-toolkit": "^1.22.0",
"indent-string": "^5.0.0",
"is-in-ci": "^1.0.0",
"patch-console": "^2.0.0",
"react-reconciler": "^0.32.0",
"scheduler": "^0.23.0",
"signal-exit": "^3.0.7",
"slice-ansi": "^7.1.0",
"stack-utils": "^2.0.6",
"string-width": "^7.2.0",
"type-fest": "^4.27.0",
"widest-line": "^5.0.0",
"wrap-ansi": "^9.0.0",
"ws": "^8.18.0",
"yoga-layout": "~3.2.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@types/react": ">=19.0.0",
"react": ">=19.0.0",
"react-devtools-core": "^4.19.1"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react-devtools-core": {
"optional": true
}
}
},
"node_modules/ink-select-input": {
"version": "6.2.0",
"license": "MIT",
"dependencies": {
"figures": "^6.1.0",
"to-rotated": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ink": ">=5.0.0",
"react": ">=18.0.0"
}
},
"node_modules/ink-text-input": {
"version": "6.0.0",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"type-fest": "^4.18.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ink": ">=5",
"react": ">=18"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-in-ci": {
"version": "1.0.0",
"license": "MIT",
"bin": {
"is-in-ci": "cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/patch-console": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/react": {
"version": "19.1.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-reconciler": {
"version": "0.32.0",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-reconciler/node_modules/scheduler": {
"version": "0.26.0",
"license": "MIT"
},
"node_modules/restore-cursor": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"license": "ISC"
},
"node_modules/slice-ansi": {
"version": "7.1.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/string-width": {
"version": "7.2.0",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/to-rotated": {
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.10.0",
"dev": true,
"license": "MIT"
},
"node_modules/widest-line": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.3",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yoga-layout": {
"version": "3.2.1",
"license": "MIT"
}
}
}
+7 -6
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "1.1.4",
"version": "1.1.5",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
@@ -11,22 +11,23 @@
"dist/"
],
"scripts": {
"start": "bun run src/ccstatusline.ts",
"build": "rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --packages=external --target-version=14",
"start": "bun run patch && bun run src/ccstatusline.ts",
"patch": "patch-package",
"build": "bun run patch && rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14",
"prepublishOnly": "bun run build"
},
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"@types/react": "^19.1.9",
"patch-package": "^8.0.0"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1"
},
"keywords": [
+17
View File
@@ -0,0 +1,17 @@
diff --git a/node_modules/ink/build/parse-keypress.js b/node_modules/ink/build/parse-keypress.js
index 1e13e81..e100611 100644
--- a/node_modules/ink/build/parse-keypress.js
+++ b/node_modules/ink/build/parse-keypress.js
@@ -161,9 +161,9 @@ const parseKeypress = (s = '') => {
key.meta = s.charAt(0) === '\x1b';
}
else if (s === '\x7f' || s === '\x1b\x7f') {
- // TODO(vadimdemedes): `enquirer` detects delete key as backspace, but I had to split them up to avoid breaking changes in Ink. Merge them back together in the next major version.
- // delete
- key.name = 'delete';
+ // On macOS, \x7f is what the backspace key sends, not delete
+ // The actual forward delete sends escape sequences like \x1b[3~
+ key.name = 'backspace';
key.meta = s.charAt(0) === '\x1b';
}
else if (s === '\x1b' || s === '\x1b\x1b') {
+23 -752
View File
@@ -1,73 +1,13 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { execSync } from 'child_process';
import { runTUI } from './tui.tsx';
import { loadSettings, type StatusItem } from './config';
import * as fs from 'fs';
import { promisify } from 'util';
// Ensure fs.promises compatibility for older Node versions
const readFile = fs.promises?.readFile || promisify(fs.readFile);
// Force chalk to use colors even when piped
chalk.level = 3;
// Helper function to apply foreground, background colors and bold
function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string {
let result = text;
// Standard color application
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
}
interface StatusJSON {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
version?: string;
}
interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
interface TranscriptLine {
message?: {
usage?: TokenUsage;
};
isSidechain?: boolean;
timestamp?: string;
}
import { loadSettings } from './utils/config';
import {
type StatusJSON,
renderStatusLine,
getTokenMetrics,
getSessionDuration,
type RenderContext
} from './utils/renderer';
async function readStdin(): Promise<string | null> {
// Check if stdin is a TTY (terminal) - if it is, there's no piped data
@@ -98,687 +38,11 @@ async function readStdin(): Promise<string | null> {
}
}
// Helper to get default color for an item type
function getItemDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty (not ?? which means no tty)
if (tty && tty !== '??' && tty !== '?') {
// Now get the terminal size
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// Command failed, width detection not available
}
// Fallback: try tput cols which might work in some environments
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
} catch {
// tput also failed
}
return null;
}
function getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
}
function getGitChanges(): { insertions: number; deletions: number } | null {
try {
let totalInsertions = 0;
let totalDeletions = 0;
// Get unstaged changes
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Get staged changes
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Parse unstaged changes
if (unstagedStat) {
const insertMatch = unstagedStat.match(/(\d+) insertion/);
const deleteMatch = unstagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Parse staged changes
if (stagedStat) {
const insertMatch = stagedStat.match(/(\d+) insertion/);
const deleteMatch = stagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Always return the changes, even if they're zero
return { insertions: totalInsertions, deletions: totalDeletions };
} catch {
return null;
}
}
async function getSessionDuration(transcriptPath: string): Promise<string | null> {
try {
if (!fs.existsSync(transcriptPath)) {
return null;
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n').filter(line => line.trim());
if (lines.length === 0) {
return null;
}
let firstTimestamp: Date | null = null;
let lastTimestamp: Date | null = null;
// Find first valid timestamp
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.timestamp) {
firstTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
// Find last valid timestamp (iterate backwards)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const data = JSON.parse(lines[i]!);
if (data.timestamp) {
lastTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
if (!firstTimestamp || !lastTimestamp) {
return null;
}
// Calculate duration in milliseconds
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
// Convert to minutes
const totalMinutes = Math.floor(durationMs / (1000 * 60));
if (totalMinutes < 1) {
return '<1m';
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours === 0) {
return `${minutes}m`;
} else if (minutes === 0) {
return `${hours}hr`;
} else {
return `${hours}hr ${minutes}m`;
}
} catch {
return null;
}
}
async function getTokenMetrics(transcriptPath: string): Promise<{
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
let contextLength = 0;
// Parse each line and sum up token usage for totals
let mostRecentMainChainEntry: TranscriptLine | null = null;
let mostRecentTimestamp: Date | null = null;
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
if (data.isSidechain !== true && data.timestamp) {
const entryTime = new Date(data.timestamp);
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
mostRecentTimestamp = entryTime;
mostRecentMainChainEntry = data;
}
}
}
} catch {
// Skip invalid JSON lines
}
}
// Calculate context length from the most recent main chain message
if (mostRecentMainChainEntry?.message?.usage) {
const usage = mostRecentMainChainEntry.message.usage;
contextLength = (usage.input_tokens || 0) +
(usage.cache_read_input_tokens || 0) +
(usage.cache_creation_input_tokens || 0);
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
}
function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, tokenMetrics: any, sessionDuration: string | null): string {
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
// Override foreground color takes precedence over EVERYTHING, including passed foreground color
let fgColor = foregroundColor;
if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING, including passed background color
let bgColor = backgroundColor;
if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings.globalBold || bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
const detectedWidth = getTerminalWidth();
// Calculate terminal width based on flex mode settings
let terminalWidth: number | null = null;
if (detectedWidth) {
const flexMode = settings.flexMode || 'full-minus-40';
if (flexMode === 'full') {
// Use full width minus 4 for terminal padding
terminalWidth = detectedWidth - 4;
} else if (flexMode === 'full-minus-40') {
// Always subtract 40 for auto-compact message
terminalWidth = detectedWidth - 40;
} else if (flexMode === 'full-until-compact') {
// Check context percentage to decide
const threshold = settings.compactThreshold || 60;
const contextPercentage = tokenMetrics ?
Math.min(100, (tokenMetrics.contextLength / 200000) * 100) : 0;
if (contextPercentage >= threshold) {
// Context is high, leave space for auto-compact
terminalWidth = detectedWidth - 40;
} else {
// Context is low, use full width minus 4 for padding
terminalWidth = detectedWidth - 4;
}
}
}
const elements: { content: string, type: string, item?: StatusItem }[] = [];
let hasFlexSeparator = false;
// Helper function to format token counts
const formatTokens = (count: number): string => {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
// Build elements based on configured items
for (const item of items) {
switch (item.type) {
case 'model':
if (data.model) {
const text = item.rawValue ? data.model.display_name : `Model: ${data.model.display_name}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
}
break;
case 'git-branch':
const branch = getGitBranch();
if (branch) {
const text = item.rawValue ? branch : `${branch}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
}
break;
case 'git-changes':
const changes = getGitChanges();
if (changes !== null) {
// Compact format: (+42,-10)
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
}
break;
case 'tokens-input':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.inputTokens) : `In: ${formatTokens(tokenMetrics.inputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.outputTokens) : `Out: ${formatTokens(tokenMetrics.outputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.cachedTokens) : `Cached: ${formatTokens(tokenMetrics.cachedTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.totalTokens) : `Total: ${formatTokens(tokenMetrics.totalTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.contextLength) : `Ctx: ${formatTokens(tokenMetrics.contextLength)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
case 'context-percentage':
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
case 'context-percentage-usable':
if (tokenMetrics) {
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (tokenMetrics.contextLength / 160000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const width = terminalWidth || getTerminalWidth();
if (width) {
const text = item.rawValue ? `${width}` : `Term: ${width}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
case 'session-clock':
if (sessionDuration) {
const text = item.rawValue ? sessionDuration : `Session: ${sessionDuration}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
case 'version':
const versionString = data.version || 'Unknown';
const versionText = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
break;
case 'separator':
// Only add separator if there are already elements and the last one isn't a separator
const lastElement = elements[elements.length - 1];
if (elements.length > 0 && lastElement && lastElement.type !== 'separator') {
const sepChar = item.character || '|';
// Handle special separator cases
let sepText;
if (sepChar === ',') {
sepText = `${sepChar} `;
} else if (sepChar === ' ') {
sepText = ' ';
} else {
sepText = ` ${sepChar} `;
}
// Use item color if specified, otherwise default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold);
elements.push({ content: sepContent, type: 'separator', item });
}
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item });
break;
case 'custom-command':
if (item.commandPath) {
try {
// Execute the command with JSON input via stdin
const timeout = item.timeout || 1000; // Default to 1000ms if not specified
const output = execSync(item.commandPath, {
encoding: 'utf8',
input: JSON.stringify(data),
stdio: ['pipe', 'pipe', 'ignore'],
timeout: timeout
}).trim();
if (output) {
let finalOutput = output;
// Handle max width truncation
if (item.maxWidth && item.maxWidth > 0) {
// Remove ANSI codes to measure actual length
const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > item.maxWidth) {
// Truncate while preserving ANSI codes
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
for (let i = 0; i < output.length; i++) {
const char = output[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < item.maxWidth) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
finalOutput = truncated;
}
}
// Apply color if not preserving original colors
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, '');
elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// Preserve original colors from command output - ignore any color property
elements.push({ content: finalOutput, type: 'custom-command', item });
}
}
} catch {
// Command failed or timed out - silently skip
}
}
break;
}
}
if (elements.length === 0) return '';
// Remove trailing separators
while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') {
elements.pop();
}
// Apply default padding and separators
const finalElements: string[] = [];
const padding = settings.defaultPadding || '';
const defaultSep = settings.defaultSeparator || '';
elements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
// Check if we should inherit colors from the previous element
if (settings.inheritSeparatorColors && index > 0) {
const prevElem = elements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (already handles override)
// Use the item's color if set, otherwise get the default color for that item type
const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type);
const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
} else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) {
// Apply override colors even when not inheriting colors
const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
} else {
// Apply padding with colors (using overrides if set)
const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none');
if (padding && (elem.item?.backgroundColor || hasColorOverride)) {
// Apply colors to padding - applyColorsWithOverride will handle the overrides
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor);
finalElements.push(paddedContent);
} else {
// No colors or no padding
finalElements.push(padding + elem.content + padding);
}
}
});
// Build the final status line
let statusLine = '';
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX' || (elements[i] && elements[i]?.type === 'flex-separator')) {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]?.push(elem!);
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
statusLine = finalElements.join('');
}
}
// Truncate if the line exceeds the terminal width
if (detectedWidth && detectedWidth > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > detectedWidth) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = detectedWidth - 7; // Truncate to width-7 for proper fit
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
}
async function renderStatusLine(data: StatusJSON) {
async function renderMultipleLines(data: StatusJSON) {
const settings = await loadSettings();
// Get all lines to render (support both old items format and new lines format)
let lines: StatusItem[][] = [];
if (settings.lines) {
lines = settings.lines;
} else if (settings.items) {
// Legacy support for single line
lines = [settings.items];
} else {
lines = [[]];
}
let lines = settings.lines || (settings.items ? [settings.items] : [[]]);
// Get token metrics if needed (check all lines)
const hasTokenItems = lines.some(line =>
@@ -792,21 +56,28 @@ async function renderStatusLine(data: StatusJSON) {
line.some(item => item.type === 'session-clock')
);
let tokenMetrics: any = null;
let tokenMetrics = null;
if (hasTokenItems && data.transcript_path) {
tokenMetrics = await getTokenMetrics(data.transcript_path);
}
let sessionDuration: string | null = null;
let sessionDuration = null;
if (hasSessionClock && data.transcript_path) {
sessionDuration = await getSessionDuration(data.transcript_path);
}
// Create render context
const context: RenderContext = {
data,
tokenMetrics,
sessionDuration,
isPreview: false
};
// Render each line
for (let i = 0; i < lines.length; i++) {
const lineItems = lines[i];
for (const lineItems of lines) {
if (lineItems && lineItems.length > 0) {
const line = renderSingleLine(lineItems, settings, data, tokenMetrics, sessionDuration);
const line = renderStatusLine(lineItems, settings, context);
// Replace all spaces with non-breaking spaces to prevent VSCode trimming
const outputLine = line.replace(/ /g, '\u00A0');
console.log(outputLine);
@@ -822,7 +93,7 @@ async function main() {
if (input && input.trim() !== '') {
try {
const data: StatusJSON = JSON.parse(input);
await renderStatusLine(data);
await renderMultipleLines(data);
} catch (error) {
console.error('Error parsing JSON:', error);
process.exit(1);
+116 -398
View File
@@ -3,10 +3,16 @@ import { render, Box, Text, useInput, useApp } from 'ink';
import SelectInput from 'ink-select-input';
import chalk from 'chalk';
import { execSync } from 'child_process';
import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './config';
import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './claude-settings';
import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './utils/config';
import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './utils/claude-settings';
import * as fs from 'fs';
import * as path from 'path';
import {
renderStatusLine as renderLine,
getItemDefaultColor,
applyColors,
type RenderContext
} from './utils/renderer';
// Get package version
function getPackageVersion(): string {
@@ -20,27 +26,8 @@ function getPackageVersion(): string {
}
// Get default color for each item type (matching ccstatusline.ts defaults)
function getDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
// Re-export from shared renderer module
const getDefaultColor = getItemDefaultColor;
// Check if terminal width detection is available
function canDetectTerminalWidth(): boolean {
@@ -92,337 +79,31 @@ interface StatusLinePreviewProps {
settings?: Settings;
}
// Helper function to apply foreground, background colors and bold
const applyColors = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
let result = text;
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
};
const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetectionAvailable: boolean, settings?: Settings): string => {
// Helper to apply colors with optional background and foreground override
const applyItemColors = (text: string, item: StatusItem, defaultColor?: string): string => {
// Override foreground color takes precedence over EVERYTHING
let fgColor = item.color || defaultColor;
if (settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING
let bgColor = item.backgroundColor;
if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings?.globalBold || item.bold;
return applyColors(text, fgColor, bgColor, shouldBold);
// Create render context for preview
const context: RenderContext = {
terminalWidth,
isPreview: true
};
// Calculate effective width based on flex mode settings
let effectiveWidth: number | null = null;
if (widthDetectionAvailable && terminalWidth) {
const flexMode = settings?.flexMode || 'full-minus-40';
if (flexMode === 'full') {
effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview
} else if (flexMode === 'full-minus-40') {
effectiveWidth = terminalWidth - 40;
} else if (flexMode === 'full-until-compact') {
// For preview, always show full width
effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview
}
}
// Always ensure we have a width for truncation
const width = terminalWidth; // Always use terminal width for truncation
const flexWidth = effectiveWidth; // Use this for flex separator calculations
const rawElements: { content: string; type: string; item?: StatusItem }[] = [];
let hasFlexSeparator = false;
items.forEach((item, index) => {
switch (item.type) {
case 'model':
const modelText = item.rawValue ? 'Claude' : 'Model: Claude';
rawElements.push({ content: applyItemColors(modelText, item, 'cyan'), type: 'model', item });
break;
case 'git-branch':
const branchText = '⎇ main';
rawElements.push({ content: applyItemColors(branchText, item, 'magenta'), type: 'git-branch', item });
break;
case 'git-changes':
const changesText = '(+42,-10)';
rawElements.push({ content: applyItemColors(changesText, item, 'yellow'), type: 'git-changes', item });
break;
case 'tokens-input':
const inputText = item.rawValue ? '15.2k' : 'In: 15.2k';
rawElements.push({ content: applyItemColors(inputText, item, 'blue'), type: 'tokens-input', item });
break;
case 'tokens-output':
const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k';
rawElements.push({ content: applyItemColors(outputText, item, 'white'), type: 'tokens-output', item });
break;
case 'tokens-cached':
const cachedText = item.rawValue ? '12k' : 'Cached: 12k';
rawElements.push({ content: applyItemColors(cachedText, item, 'cyan'), type: 'tokens-cached', item });
break;
case 'tokens-total':
const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k';
rawElements.push({ content: applyItemColors(totalText, item, 'cyan'), type: 'tokens-total', item });
break;
case 'context-length':
const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k';
rawElements.push({ content: applyItemColors(ctxText, item, 'gray'), type: 'context-length', item });
break;
case 'context-percentage':
const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%';
rawElements.push({ content: applyItemColors(ctxPctText, item, 'blue'), type: 'context-percentage', item });
break;
case 'context-percentage-usable':
const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
rawElements.push({ content: applyItemColors(ctxUsableText, item, 'green'), type: 'context-percentage-usable', item });
break;
case 'session-clock':
const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
rawElements.push({ content: applyItemColors(sessionText, item, 'yellow'), type: 'session-clock', item });
break;
case 'version':
const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72';
rawElements.push({ content: applyItemColors(versionText, item, 'green'), type: 'version', item });
break;
case 'terminal-width':
const detectedWidth = canDetectTerminalWidth() ? terminalWidth : '??';
const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
rawElements.push({ content: applyItemColors(termText, item, 'gray'), type: 'terminal-width', item });
break;
case 'separator':
const sepChar = item.character || '|';
// Handle special separator cases
let sepContent;
if (sepChar === ',') {
sepContent = `${sepChar} `;
} else if (sepChar === ' ') {
sepContent = ' ';
} else {
sepContent = ` ${sepChar} `;
}
rawElements.push({ content: applyItemColors(sepContent, item, 'gray'), type: 'separator', item });
break;
case 'flex-separator':
rawElements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
rawElements.push({ content: applyItemColors(customText, item, 'white'), type: 'custom-text', item });
break;
case 'custom-command':
const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
// Only apply color if not preserving colors
if (!item.preserveColors) {
rawElements.push({ content: applyItemColors(cmdText, item, 'white'), type: 'custom-command', item });
} else {
// When preserving colors, just show the text without applying our colors
rawElements.push({ content: cmdText, type: 'custom-command', item });
}
break;
}
});
// Apply default padding and separators
const elements: string[] = [];
const padding = settings?.defaultPadding || '';
const defaultSep = settings?.defaultSeparator || '';
// Debug: Check if we have a default separator
// console.log('Default separator:', JSON.stringify(defaultSep), 'Length:', defaultSep.length);
rawElements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
// Check if we should inherit colors from the previous element
if (settings?.inheritSeparatorColors && index > 0) {
const prevElem = rawElements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (with overrides)
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: (prevElem.item.color || getDefaultColor(prevElem.item.type));
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: prevElem.item.backgroundColor;
const shouldBold = settings?.globalBold || prevElem.item.bold;
const coloredSep = applyColors(defaultSep, fgColor, bgColor, shouldBold);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
} else if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none' ||
settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
// Apply override colors even when not inheriting colors
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: undefined;
const coloredSep = applyColors(defaultSep, fgColor, settings.overrideBackgroundColor, settings?.globalBold);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
}
// Add the element itself
if (elem.type === 'separator' || elem.type === 'flex-separator') {
elements.push(elem.content);
} else {
// Apply padding with the same colors as the item (or overrides)
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: undefined;
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: elem.item?.backgroundColor;
if (padding && (bgColor || fgColor)) {
// Apply colors to padding
const paddedContent = applyColors(padding, fgColor, bgColor, settings?.globalBold) +
elem.content +
applyColors(padding, fgColor, bgColor, settings?.globalBold);
elements.push(paddedContent);
} else {
// No colors or no padding
elements.push(padding + elem.content + padding);
}
}
});
// Build the status line with flex separator support
let statusLine = '';
if (hasFlexSeparator && flexWidth) {
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item && item.type === 'flex-separator') {
currentPart++;
parts[currentPart] = [];
} else {
const element = elements[i];
if (element !== 'FLEX' && parts[currentPart]) {
parts[currentPart]!.push(element as string);
}
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, flexWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No width detection available - treat flex separators as normal separators
statusLine = elements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
}
// Truncate if the line exceeds the maximum width
if (width && width > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > width) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = width - 5; // Reserve 3 chars for ellipsis + 2 for proper fit in preview
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
return renderLine(items, settings || {}, context);
};
const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ lines, terminalWidth, settings }) => {
const widthDetectionAvailable = canDetectTerminalWidth();
// Build the Claude Code input box - account for ink's padding
const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
const topLine = chalk.gray('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮');
const middleLine = chalk.gray('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.gray('│');
const bottomLine = chalk.gray('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯');
// Render each configured line - account for 2-space prefix in display
const availableWidth = boxWidth - 2; // Account for 2-space indent
// Render each configured line
// Pass the full terminal width - the renderer will handle preview adjustments
const renderedLines = lines.map(lineItems =>
lineItems.length > 0 ? renderSingleLine(lineItems, availableWidth, widthDetectionAvailable, settings) : ''
lineItems.length > 0 ? renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings) : ''
).filter(line => line !== ''); // Remove empty lines
return (
<Box flexDirection='column'>
<Text>{topLine}</Text>
<Text>{middleLine}</Text>
<Text>{bottomLine}</Text>
<Box borderStyle="round" borderColor="gray" width="100%" paddingLeft={1}>
<Text>&gt;</Text>
</Box>
{renderedLines.map((line, index) => (
<Text key={index}> {line}</Text>
))}
@@ -518,7 +199,7 @@ const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasCha
const items = [
{ label: '📝 Edit Lines', value: 'lines' },
{ label: '🎨 Configure Colors', value: 'colors' },
{ label: '⚡ Flex Options', value: 'flex' },
{ label: '📏 Terminal Width Options', value: 'terminalWidth' },
{ label: '🔧 Global Options', value: 'globalOptions' },
{ label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' },
];
@@ -536,9 +217,9 @@ const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasCha
<Box flexDirection='column'>
<Text bold>Main Menu</Text>
<Box marginTop={1}>
<SelectInput
items={items}
onSelect={(item) => onSelect(item.value)}
<SelectInput
items={items}
onSelect={(item) => onSelect(item.value)}
initialIndex={Math.min(initialSelection, items.length - 1)}
/>
</Box>
@@ -597,11 +278,15 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
} else if (key.ctrl && key.rightArrow) {
// Move to end
setTextCursorPos(textInput.length);
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
if (textCursorPos > 0) {
setTextInput(textInput.slice(0, textCursorPos - 1) + textInput.slice(textCursorPos));
setTextCursorPos(textCursorPos - 1);
}
} else if (key.delete) {
if (textCursorPos < textInput.length) {
setTextInput(textInput.slice(0, textCursorPos) + textInput.slice(textCursorPos + 1));
}
} else if (input && input.length === 1) {
setTextInput(textInput.slice(0, textCursorPos) + input + textInput.slice(textCursorPos));
setTextCursorPos(textCursorPos + 1);
@@ -634,11 +319,15 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
} else if (key.ctrl && key.rightArrow) {
// Move to end
setCommandCursorPos(commandInput.length);
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
if (commandCursorPos > 0) {
setCommandInput(commandInput.slice(0, commandCursorPos - 1) + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos - 1);
}
} else if (key.delete) {
if (commandCursorPos < commandInput.length) {
setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1));
}
} else if (input) {
setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos + input.length);
@@ -666,8 +355,10 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Cancel editing
setEditingMaxWidth(false);
setMaxWidthInput('');
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
setMaxWidthInput(maxWidthInput.slice(0, -1));
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
setMaxWidthInput(maxWidthInput + input);
}
@@ -694,8 +385,10 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Cancel editing
setEditingTimeout(false);
setTimeoutInput('');
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
setTimeoutInput(timeoutInput.slice(0, -1));
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
setTimeoutInput(timeoutInput + input);
}
@@ -864,7 +557,7 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Get the color for this item (use custom color if set, otherwise default)
const colorName = item.color || getDefaultColor(item.type);
const colorFunc = (chalk as any)[colorName] || chalk.white;
switch (item.type) {
case 'model':
return colorFunc('Model');
@@ -1012,10 +705,16 @@ interface ColorMenuProps {
}
const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const colorableItems = items.filter(item =>
['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors) // Exclude custom-command items with preserveColors
);
const [showSeparators, setShowSeparators] = useState(false);
const colorableItems = items.filter(item => {
// Include separators only if showSeparators is true
if (item.type === 'separator') {
return showSeparators;
}
return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors); // Exclude custom-command items with preserveColors
});
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(colorableItems[0]?.id || null);
const [editingBackground, setEditingBackground] = useState(false);
@@ -1028,7 +727,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
onBack();
return;
}
// Normal keyboard handling when there are items
if (key.escape) {
if (editingBackground) {
@@ -1036,6 +735,20 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
} else {
onBack();
}
} else if (input === 's' || input === 'S') {
// Toggle show separators
setShowSeparators(!showSeparators);
// Reset highlight to first item if there are items
const newColorableItems = items.filter(item => {
if (item.type === 'separator') {
return !showSeparators; // Will be toggled
}
return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors);
});
if (newColorableItems.length > 0) {
setHighlightedItemId(newColorableItems[0]!.id);
}
} else if (input === 'f' || input === 'F') {
if (colorableItems.length > 0) {
setEditingBackground(!editingBackground);
@@ -1099,6 +812,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
case 'session-clock': return 'Session Clock';
case 'terminal-width': return 'Terminal Width';
case 'version': return 'Version';
case 'separator': return `Separator (${item.character || '|'})`;
case 'custom-text': return `Custom Text (${item.customText || 'Empty'})`;
case 'custom-command': {
const cmd = item.commandPath ? item.commandPath.substring(0, 20) + (item.commandPath.length > 20 ? '...' : '') : 'No command';
@@ -1167,25 +881,25 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
'magentaBright', 'cyanBright', 'whiteBright'];
// Background colors list (includes 'none' as first option)
// Note: chalk doesn't support bgDim, so we exclude it from background colors
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
// Get current color for highlighted item
const selectedItem = highlightedItemId && highlightedItemId !== 'back'
? colorableItems.find(item => item.id === highlightedItemId)
const selectedItem = highlightedItemId && highlightedItemId !== 'back'
? colorableItems.find(item => item.id === highlightedItemId)
: null;
const currentColor = editingBackground
const currentColor = editingBackground
? (selectedItem?.backgroundColor || 'none')
: (selectedItem ? (selectedItem.color || getDefaultColor(selectedItem.type)) : 'white');
const colorList = editingBackground ? bgColors : colors;
const colorIndex = colorList.indexOf(currentColor);
const colorNumber = colorIndex === -1 ? (editingBackground ? 1 : 8) : colorIndex + 1;
let colorDisplay;
if (editingBackground) {
if (currentColor === 'none') {
@@ -1203,6 +917,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
<Box flexDirection='column'>
<Text bold>Configure Colors {editingBackground && chalk.yellow('[Background Mode]')}</Text>
<Text dimColor> to select, Enter to cycle {editingBackground ? 'background' : 'foreground'}, (f) to toggle bg/fg, (b)old, (r)eset, ESC to go back</Text>
<Text dimColor>(s)how separators: {showSeparators ? chalk.green('ON') : chalk.gray('OFF')}</Text>
{selectedItem && (
<Box marginTop={1}>
<Text>
@@ -1221,21 +936,19 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
</Box>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'> VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
);
};
interface FlexOptionsProps {
interface TerminalWidthOptionsProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack }) => {
const TerminalWidthOptions: React.FC<TerminalWidthOptionsProps> = ({ settings, onUpdate, onBack }) => {
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode || 'full-minus-40');
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 60);
const [editingThreshold, setEditingThreshold] = useState(false);
@@ -1267,9 +980,11 @@ const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack })
setThresholdInput(String(compactThreshold));
setEditingThreshold(false);
setValidationError(null);
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
setThresholdInput(thresholdInput.slice(0, -1));
setValidationError(null);
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
const newValue = thresholdInput + input;
if (newValue.length <= 2) {
@@ -1292,7 +1007,7 @@ const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack })
},
{
value: 'full-minus-40' as FlexMode,
label: 'Full width minus 40',
label: 'Full width minus 40 (default)',
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
},
{
@@ -1330,8 +1045,9 @@ const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack })
return (
<Box flexDirection='column'>
<Text bold>Flex Separator Width Options</Text>
<Text dimColor>Select how flex separators calculate available width</Text>
<Text bold>Terminal Width Options</Text>
<Text color="white">These settings affect where long lines are truncated, and where right-alignment occurs when using flex separators</Text>
<Text dimColor wrap="wrap">These settings are necessary because claude code does not currently provide an available width variable for the statusline and features like IDE integration, auto-compaction notices, and rate limit messages can all cause the statusline to wrap if we do not truncate it</Text>
{editingThreshold ? (
<Box marginTop={1} flexDirection='column'>
@@ -1393,13 +1109,13 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator || '');
const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors || false);
const [globalBold, setGlobalBold] = useState(settings.globalBold || false);
// Background color override
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
const currentBgIndex = bgColors.indexOf(settings.overrideBackgroundColor || 'none');
// Foreground color override
const fgColors = ['none', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
@@ -1418,8 +1134,10 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
} else if (key.escape) {
setPaddingInput(settings.defaultPadding || '');
setEditingPadding(false);
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
setPaddingInput(paddingInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setPaddingInput(paddingInput + input);
}
@@ -1434,8 +1152,10 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
} else if (key.escape) {
setSeparatorInput(settings.defaultSeparator || '');
setEditingSeparator(false);
} else if (key.backspace || key.delete) {
} else if (key.backspace) {
setSeparatorInput(separatorInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setSeparatorInput(separatorInput + input);
}
@@ -1504,7 +1224,7 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
<Text bold>Global Options</Text>
<Text dimColor>Configure automatic padding and separators between items</Text>
<Box marginTop={1} />
{editingPadding ? (
<Box flexDirection="column">
<Box>
@@ -1528,25 +1248,25 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
<Text color="cyan">{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
<Text dimColor> - Press (p) to edit</Text>
</Box>
<Box>
<Text>Default Separator: </Text>
<Text color="cyan">{settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'}</Text>
<Text dimColor> - Press (s) to edit</Text>
</Box>
<Box>
<Text> Inherit Colors: </Text>
<Text color={inheritColors ? "green" : "red"}>{inheritColors ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (i) to toggle</Text>
</Box>
<Box>
<Text> Global Bold: </Text>
<Text color={globalBold ? "green" : "red"}>{globalBold ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (o) to toggle</Text>
</Box>
<Box>
<Text>Override BG Color: </Text>
{(() => {
@@ -1562,7 +1282,7 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
})()}
<Text dimColor> - (b) cycle, (c) clear</Text>
</Box>
<Box>
<Text>Override FG Color: </Text>
{(() => {
@@ -1577,11 +1297,11 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
})()}
<Text dimColor> - (f) cycle, (g) clear</Text>
</Box>
<Box marginTop={2}>
<Text dimColor>Press ESC to go back</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Text dimColor wrap='wrap'>
Note: These settings are applied during rendering and don't add items to your widget list.
@@ -1597,9 +1317,7 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
</Text>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'>⚠ VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
</>
@@ -1613,7 +1331,7 @@ const App: React.FC = () => {
const [settings, setSettings] = useState<Settings | null>(null);
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
const [hasChanges, setHasChanges] = useState(false);
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'flex' | 'globalOptions' | 'confirm'>('main');
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'terminalWidth' | 'globalOptions' | 'confirm'>('main');
const [selectedLine, setSelectedLine] = useState(0);
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
@@ -1709,8 +1427,8 @@ const App: React.FC = () => {
case 'colors':
setScreen('colors');
break;
case 'flex':
setScreen('flex');
case 'terminalWidth':
setScreen('terminalWidth');
break;
case 'globalOptions':
setScreen('globalOptions');
@@ -1754,14 +1472,14 @@ const App: React.FC = () => {
<Box marginTop={2}>
{screen === 'main' && (
<MainMenu
<MainMenu
onSelect={(value) => {
// Only persist menu selection if not exiting
if (value !== 'save' && value !== 'exit') {
const menuMap: Record<string, number> = {
'lines': 0,
'colors': 1,
'flex': 2,
'terminalWidth': 2,
'globalOptions': 3,
'install': 4
};
@@ -1769,7 +1487,7 @@ const App: React.FC = () => {
}
handleMainMenuSelect(value);
}}
isClaudeInstalled={isClaudeInstalled}
isClaudeInstalled={isClaudeInstalled}
hasChanges={hasChanges}
initialSelection={menuSelections.main || 0}
/>
@@ -1831,14 +1549,14 @@ const App: React.FC = () => {
}}
/>
)}
{screen === 'flex' && (
<FlexOptions
{screen === 'terminalWidth' && (
<TerminalWidthOptions
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
// Save that we came from 'flex' menu (index 2)
// Save that we came from 'terminalWidth' menu (index 2)
setMenuSelections({ ...menuSelections, main: 2 });
setScreen('main');
}}
+826
View File
@@ -0,0 +1,826 @@
import chalk from 'chalk';
import { execSync } from 'child_process';
import * as fs from 'fs';
import { promisify } from 'util';
import type { StatusItem } from './config';
// Ensure fs.promises compatibility for older Node versions
const readFile = fs.promises?.readFile || promisify(fs.readFile);
// Force chalk to use colors even when piped
chalk.level = 3;
export interface StatusJSON {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
version?: string;
}
export interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
export interface TranscriptLine {
message?: {
usage?: TokenUsage;
};
isSidechain?: boolean;
timestamp?: string;
}
export interface TokenMetrics {
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}
export interface RenderContext {
data?: StatusJSON;
tokenMetrics?: TokenMetrics | null;
sessionDuration?: string | null;
gitBranch?: string | null;
gitChanges?: { insertions: number; deletions: number } | null;
terminalWidth?: number | null;
isPreview?: boolean;
}
// Helper function to apply foreground, background colors and bold
export function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string {
let result = text;
// Standard color application
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
}
// Helper to get default color for an item type
export function getItemDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
// Helper function to format token counts
export function formatTokens(count: number): string {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
}
export function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty (not ?? which means no tty)
if (tty && tty !== '??' && tty !== '?') {
// Now get the terminal size
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// Command failed, width detection not available
}
// Fallback: try tput cols which might work in some environments
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
} catch {
// tput also failed
}
return null;
}
export function getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
}
export function getGitChanges(): { insertions: number; deletions: number } | null {
try {
let totalInsertions = 0;
let totalDeletions = 0;
// Get unstaged changes
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Get staged changes
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Parse unstaged changes
if (unstagedStat) {
const insertMatch = unstagedStat.match(/(\d+) insertion/);
const deleteMatch = unstagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Parse staged changes
if (stagedStat) {
const insertMatch = stagedStat.match(/(\d+) insertion/);
const deleteMatch = stagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Always return the changes, even if they're zero
return { insertions: totalInsertions, deletions: totalDeletions };
} catch {
return null;
}
}
export async function getSessionDuration(transcriptPath: string): Promise<string | null> {
try {
if (!fs.existsSync(transcriptPath)) {
return null;
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n').filter(line => line.trim());
if (lines.length === 0) {
return null;
}
let firstTimestamp: Date | null = null;
let lastTimestamp: Date | null = null;
// Find first valid timestamp
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.timestamp) {
firstTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
// Find last valid timestamp (iterate backwards)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const data = JSON.parse(lines[i]!);
if (data.timestamp) {
lastTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
if (!firstTimestamp || !lastTimestamp) {
return null;
}
// Calculate duration in milliseconds
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
// Convert to minutes
const totalMinutes = Math.floor(durationMs / (1000 * 60));
if (totalMinutes < 1) {
return '<1m';
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours === 0) {
return `${minutes}m`;
} else if (minutes === 0) {
return `${hours}hr`;
} else {
return `${hours}hr ${minutes}m`;
}
} catch {
return null;
}
}
export async function getTokenMetrics(transcriptPath: string): Promise<TokenMetrics> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
let contextLength = 0;
// Parse each line and sum up token usage for totals
let mostRecentMainChainEntry: TranscriptLine | null = null;
let mostRecentTimestamp: Date | null = null;
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
if (data.isSidechain !== true && data.timestamp) {
const entryTime = new Date(data.timestamp);
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
mostRecentTimestamp = entryTime;
mostRecentMainChainEntry = data;
}
}
}
} catch {
// Skip invalid JSON lines
}
}
// Calculate context length from the most recent main chain message
if (mostRecentMainChainEntry?.message?.usage) {
const usage = mostRecentMainChainEntry.message.usage;
contextLength = (usage.input_tokens || 0) +
(usage.cache_read_input_tokens || 0) +
(usage.cache_creation_input_tokens || 0);
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
}
export function renderStatusLine(
items: StatusItem[],
settings: any,
context: RenderContext
): string {
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
// Override foreground color takes precedence over EVERYTHING, including passed foreground color
let fgColor = foregroundColor;
if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING, including passed background color
let bgColor = backgroundColor;
if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings.globalBold || bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
const detectedWidth = context.terminalWidth || getTerminalWidth();
// Calculate terminal width based on flex mode settings
let terminalWidth: number | null = null;
if (detectedWidth) {
const flexMode = settings.flexMode || 'full-minus-40';
if (context.isPreview) {
// In preview mode, account for box borders and padding (6 chars total)
if (flexMode === 'full') {
terminalWidth = detectedWidth - 6; // Subtract 6 for box borders and padding in preview
} else if (flexMode === 'full-minus-40') {
terminalWidth = detectedWidth - 43; // -40 for auto-compact + 3 for preview
} else if (flexMode === 'full-until-compact') {
// For preview, always show full width minus preview padding
terminalWidth = detectedWidth - 6;
}
} else {
// In actual rendering mode
if (flexMode === 'full') {
// Use full width minus 4 for terminal padding
terminalWidth = detectedWidth - 4;
} else if (flexMode === 'full-minus-40') {
// Always subtract 41 for auto-compact message
terminalWidth = detectedWidth - 41;
} else if (flexMode === 'full-until-compact') {
// Check context percentage to decide
const threshold = settings.compactThreshold || 60;
const contextPercentage = context.tokenMetrics ?
Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100) : 0;
if (contextPercentage >= threshold) {
// Context is high, leave space for auto-compact
terminalWidth = detectedWidth - 40;
} else {
// Context is low, use full width minus 4 for padding
terminalWidth = detectedWidth - 4;
}
}
}
}
const elements: { content: string, type: string, item?: StatusItem }[] = [];
let hasFlexSeparator = false;
// Build elements based on configured items
for (const item of items) {
switch (item.type) {
case 'model':
if (context.isPreview) {
const modelText = item.rawValue ? 'Claude' : 'Model: Claude';
elements.push({ content: applyColorsWithOverride(modelText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
} else if (context.data?.model) {
const text = item.rawValue ? context.data.model.display_name : `Model: ${context.data.model.display_name}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
}
break;
case 'git-branch':
if (context.isPreview) {
const branchText = item.rawValue ? 'main' : '⎇ main';
elements.push({ content: applyColorsWithOverride(branchText, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
} else {
const branch = context.gitBranch || getGitBranch();
if (branch) {
const text = item.rawValue ? branch : `${branch}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
}
}
break;
case 'git-changes':
if (context.isPreview) {
const changesText = '(+42,-10)';
elements.push({ content: applyColorsWithOverride(changesText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
} else {
const changes = context.gitChanges || getGitChanges();
if (changes !== null) {
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
}
}
break;
case 'tokens-input':
if (context.isPreview) {
const inputText = item.rawValue ? '15.2k' : 'In: 15.2k';
elements.push({ content: applyColorsWithOverride(inputText, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.inputTokens) : `In: ${formatTokens(context.tokenMetrics.inputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (context.isPreview) {
const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k';
elements.push({ content: applyColorsWithOverride(outputText, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.outputTokens) : `Out: ${formatTokens(context.tokenMetrics.outputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (context.isPreview) {
const cachedText = item.rawValue ? '12k' : 'Cached: 12k';
elements.push({ content: applyColorsWithOverride(cachedText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.cachedTokens) : `Cached: ${formatTokens(context.tokenMetrics.cachedTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (context.isPreview) {
const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k';
elements.push({ content: applyColorsWithOverride(totalText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.totalTokens) : `Total: ${formatTokens(context.tokenMetrics.totalTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (context.isPreview) {
const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k';
elements.push({ content: applyColorsWithOverride(ctxText, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.contextLength) : `Ctx: ${formatTokens(context.tokenMetrics.contextLength)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
case 'context-percentage':
if (context.isPreview) {
const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%';
elements.push({ content: applyColorsWithOverride(ctxPctText, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
} else if (context.tokenMetrics) {
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
case 'context-percentage-usable':
if (context.isPreview) {
const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
elements.push({ content: applyColorsWithOverride(ctxUsableText, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
} else if (context.tokenMetrics) {
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 160000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const width = terminalWidth || getTerminalWidth();
if (context.isPreview) {
const detectedWidth = width || '??';
const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
elements.push({ content: applyColorsWithOverride(termText, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
} else if (width) {
const text = item.rawValue ? `${width}` : `Term: ${width}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
case 'session-clock':
if (context.isPreview) {
const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
elements.push({ content: applyColorsWithOverride(sessionText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
} else if (context.sessionDuration) {
const text = item.rawValue ? context.sessionDuration : `Session: ${context.sessionDuration}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
case 'version':
if (context.isPreview) {
const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72';
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
} else if (context.data?.version) {
const versionString = context.data.version || 'Unknown';
const versionText = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
}
break;
case 'separator':
// Always add separators - users should be able to add multiple if they want
const sepChar = item.character || '|';
// Handle special separator cases
let sepText;
if (sepChar === ',') {
sepText = `${sepChar} `;
} else if (sepChar === ' ') {
sepText = ' ';
} else {
sepText = ` ${sepChar} `;
}
// Use item color if specified, otherwise default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold);
elements.push({ content: sepContent, type: 'separator', item });
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item });
break;
case 'custom-command':
if (context.isPreview) {
const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
// Only apply color if not preserving colors
if (!item.preserveColors) {
elements.push({ content: applyColorsWithOverride(cmdText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// When preserving colors, just show the text without applying our colors
elements.push({ content: cmdText, type: 'custom-command', item });
}
} else if (item.commandPath && context.data) {
try {
// Execute the command with JSON input via stdin
const timeout = item.timeout || 1000; // Default to 1000ms if not specified
const output = execSync(item.commandPath, {
encoding: 'utf8',
input: JSON.stringify(context.data),
stdio: ['pipe', 'pipe', 'ignore'],
timeout: timeout
}).trim();
if (output) {
let finalOutput = output;
// Handle max width truncation
if (item.maxWidth && item.maxWidth > 0) {
// Remove ANSI codes to measure actual length
const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > item.maxWidth) {
// Truncate while preserving ANSI codes
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
for (let i = 0; i < output.length; i++) {
const char = output[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < item.maxWidth) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
finalOutput = truncated;
}
}
// Apply color if not preserving original colors
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, '');
elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// Preserve original colors from command output - ignore any color property
elements.push({ content: finalOutput, type: 'custom-command', item });
}
}
} catch {
// Command failed or timed out - silently skip
}
}
break;
}
}
if (elements.length === 0) return '';
// Remove trailing separators
while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') {
elements.pop();
}
// Apply default padding and separators
const finalElements: string[] = [];
const padding = settings.defaultPadding || '';
const defaultSep = settings.defaultSeparator || '';
elements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item, and not around flex separators)
const prevElem = index > 0 ? elements[index - 1] : null;
const shouldAddSeparator = defaultSep && index > 0 &&
elem.type !== 'flex-separator' &&
prevElem?.type !== 'flex-separator';
if (shouldAddSeparator) {
// Check if we should inherit colors from the previous element
if (settings.inheritSeparatorColors && index > 0) {
const prevElem = elements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (already handles override)
// Use the item's color if set, otherwise get the default color for that item type
const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type);
const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
} else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) {
// Apply override colors even when not inheriting colors
const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
} else {
// Apply padding with colors (using overrides if set)
const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none');
if (padding && (elem.item?.backgroundColor || hasColorOverride)) {
// Apply colors to padding - applyColorsWithOverride will handle the overrides
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor);
finalElements.push(paddedContent);
} else {
// No colors or no padding
finalElements.push(padding + elem.content + padding);
}
}
});
// Build the final status line
let statusLine = '';
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX') {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]?.push(elem!);
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
statusLine = finalElements.join('');
}
}
// Truncate if the line exceeds the terminal width
// Use terminalWidth if available (already accounts for flex mode adjustments), otherwise use detectedWidth
const maxWidth = terminalWidth || detectedWidth;
if (maxWidth && maxWidth > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > maxWidth) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = context.isPreview ? maxWidth - 3 : maxWidth - 3; // Reserve 3 chars for ellipsis
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
}