ccstatusline 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -32
- package/dist/ccstatusline.js +374 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,12 @@ A customizable status line formatter for Claude Code CLI that displays model inf
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 📊 **Real-time metrics** - Display model name, git branch, token usage, and more
|
|
7
|
+
- 📊 **Real-time metrics** - Display model name, git branch, token usage, session duration, and more
|
|
8
8
|
- 🎨 **Fully customizable** - Choose what to display and customize colors
|
|
9
|
+
- 📐 **Multi-line support** - Configure up to 3 status lines
|
|
9
10
|
- 🖥️ **Interactive TUI** - Built-in configuration interface using React/Ink
|
|
10
11
|
- 🚀 **Cross-platform** - Works with both Bun and Node.js
|
|
11
|
-
- 📏 **
|
|
12
|
+
- 📏 **Auto-width detection** - Automatically adapts to terminal width with flex separators
|
|
12
13
|
|
|
13
14
|
## Quick Start
|
|
14
15
|
|
|
@@ -45,12 +46,15 @@ Once configured, ccstatusline automatically formats your Claude Code status line
|
|
|
45
46
|
|
|
46
47
|
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
|
|
47
48
|
- **Git Branch** - Displays current git branch name
|
|
49
|
+
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
|
|
50
|
+
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
|
|
48
51
|
- **Tokens Input** - Shows input tokens used
|
|
49
52
|
- **Tokens Output** - Shows output tokens used
|
|
50
53
|
- **Tokens Cached** - Shows cached tokens used
|
|
51
54
|
- **Tokens Total** - Shows total tokens used
|
|
52
55
|
- **Context Length** - Shows current context length in tokens
|
|
53
56
|
- **Context Percentage** - Shows percentage of context limit used
|
|
57
|
+
- **Terminal Width** - Shows detected terminal width (for debugging)
|
|
54
58
|
- **Separator** - Visual divider between items (|)
|
|
55
59
|
- **Flex Separator** - Expands to fill available space
|
|
56
60
|
|
|
@@ -60,36 +64,38 @@ The configuration file at `~/.config/ccstatusline/settings.json` looks like:
|
|
|
60
64
|
|
|
61
65
|
```json
|
|
62
66
|
{
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
67
|
+
"lines": [
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
"id": "1",
|
|
71
|
+
"type": "model",
|
|
72
|
+
"color": "cyan"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "2",
|
|
76
|
+
"type": "separator"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "3",
|
|
80
|
+
"type": "git-branch",
|
|
81
|
+
"color": "magenta"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "4",
|
|
85
|
+
"type": "separator"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "5",
|
|
89
|
+
"type": "session-clock",
|
|
90
|
+
"color": "blue"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
],
|
|
94
|
+
"colors": {
|
|
95
|
+
"model": "cyan",
|
|
96
|
+
"gitBranch": "magenta",
|
|
97
|
+
"separator": "dim"
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
```
|
|
95
101
|
|
package/dist/ccstatusline.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/ccstatusline.ts
|
|
4
4
|
import chalk2 from "chalk";
|
|
5
|
-
import { execSync } from "child_process";
|
|
5
|
+
import { execSync as execSync2 } from "child_process";
|
|
6
6
|
|
|
7
7
|
// src/tui.tsx
|
|
8
8
|
import { useState, useEffect } from "react";
|
|
9
9
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
10
10
|
import SelectInput from "ink-select-input";
|
|
11
11
|
import chalk from "chalk";
|
|
12
|
+
import { execSync } from "child_process";
|
|
12
13
|
|
|
13
14
|
// src/config.ts
|
|
14
15
|
import * as fs from "fs";
|
|
@@ -21,12 +22,16 @@ var mkdir2 = fs.promises?.mkdir || promisify(fs.mkdir);
|
|
|
21
22
|
var CONFIG_DIR = path.join(os.homedir(), ".config", "ccstatusline");
|
|
22
23
|
var SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
23
24
|
var DEFAULT_SETTINGS = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
lines: [
|
|
26
|
+
[
|
|
27
|
+
{ id: "1", type: "model", color: "cyan" },
|
|
28
|
+
{ id: "2", type: "separator" },
|
|
29
|
+
{ id: "3", type: "terminal-width", color: "dim" },
|
|
30
|
+
{ id: "4", type: "separator" },
|
|
31
|
+
{ id: "5", type: "git-branch", color: "magenta" },
|
|
32
|
+
{ id: "6", type: "separator" },
|
|
33
|
+
{ id: "7", type: "git-changes", color: "yellow" }
|
|
34
|
+
]
|
|
30
35
|
],
|
|
31
36
|
colors: {
|
|
32
37
|
model: "cyan",
|
|
@@ -40,12 +45,29 @@ async function loadSettings() {
|
|
|
40
45
|
return DEFAULT_SETTINGS;
|
|
41
46
|
}
|
|
42
47
|
const content = await readFile2(SETTINGS_PATH, "utf-8");
|
|
43
|
-
|
|
48
|
+
let loaded;
|
|
49
|
+
try {
|
|
50
|
+
loaded = JSON.parse(content);
|
|
51
|
+
} catch (parseError) {
|
|
52
|
+
console.error("Failed to parse settings.json, using defaults");
|
|
53
|
+
return DEFAULT_SETTINGS;
|
|
54
|
+
}
|
|
44
55
|
if (loaded.elements || loaded.layout) {
|
|
45
56
|
return migrateOldSettings(loaded);
|
|
46
57
|
}
|
|
58
|
+
if (loaded.items && !loaded.lines) {
|
|
59
|
+
loaded.lines = [loaded.items];
|
|
60
|
+
delete loaded.items;
|
|
61
|
+
}
|
|
62
|
+
if (loaded.lines) {
|
|
63
|
+
if (!Array.isArray(loaded.lines)) {
|
|
64
|
+
loaded.lines = [[]];
|
|
65
|
+
}
|
|
66
|
+
loaded.lines = loaded.lines.slice(0, 3);
|
|
67
|
+
}
|
|
47
68
|
return { ...DEFAULT_SETTINGS, ...loaded };
|
|
48
|
-
} catch {
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Error loading settings:", error);
|
|
49
71
|
return DEFAULT_SETTINGS;
|
|
50
72
|
}
|
|
51
73
|
}
|
|
@@ -69,7 +91,7 @@ function migrateOldSettings(old) {
|
|
|
69
91
|
});
|
|
70
92
|
}
|
|
71
93
|
return {
|
|
72
|
-
items,
|
|
94
|
+
lines: [items],
|
|
73
95
|
colors: old.colors || DEFAULT_SETTINGS.colors
|
|
74
96
|
};
|
|
75
97
|
}
|
|
@@ -105,14 +127,14 @@ async function saveClaudeSettings(settings) {
|
|
|
105
127
|
}
|
|
106
128
|
async function isInstalled() {
|
|
107
129
|
const settings = await loadClaudeSettings();
|
|
108
|
-
return settings.statusLine?.command === "npx -y ccstatusline@latest";
|
|
130
|
+
return settings.statusLine?.command === "npx -y ccstatusline@latest" && (settings.statusLine.padding === 0 || settings.statusLine.padding === undefined);
|
|
109
131
|
}
|
|
110
132
|
async function installStatusLine() {
|
|
111
133
|
const settings = await loadClaudeSettings();
|
|
112
134
|
settings.statusLine = {
|
|
113
135
|
type: "command",
|
|
114
136
|
command: "npx -y ccstatusline@latest",
|
|
115
|
-
padding:
|
|
137
|
+
padding: 0
|
|
116
138
|
};
|
|
117
139
|
await saveClaudeSettings(settings);
|
|
118
140
|
}
|
|
@@ -130,8 +152,38 @@ async function getExistingStatusLine() {
|
|
|
130
152
|
|
|
131
153
|
// src/tui.tsx
|
|
132
154
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
function canDetectTerminalWidth() {
|
|
156
|
+
try {
|
|
157
|
+
const tty = execSync("ps -o tty= -p $(ps -o ppid= -p $$)", {
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
160
|
+
shell: "/bin/sh"
|
|
161
|
+
}).trim();
|
|
162
|
+
if (tty && tty !== "??" && tty !== "?") {
|
|
163
|
+
const width = execSync(`stty size < /dev/${tty} | awk '{print $2}'`, {
|
|
164
|
+
encoding: "utf8",
|
|
165
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
166
|
+
shell: "/bin/sh"
|
|
167
|
+
}).trim();
|
|
168
|
+
const parsed = parseInt(width, 10);
|
|
169
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
try {
|
|
175
|
+
const width = execSync("tput cols 2>/dev/null", {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
178
|
+
}).trim();
|
|
179
|
+
const parsed = parseInt(width, 10);
|
|
180
|
+
return !isNaN(parsed) && parsed > 0;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
var renderSingleLine = (items, terminalWidth, widthDetectionAvailable) => {
|
|
186
|
+
const width = widthDetectionAvailable ? terminalWidth : null;
|
|
135
187
|
const elements = [];
|
|
136
188
|
let hasFlexSeparator = false;
|
|
137
189
|
items.forEach((item) => {
|
|
@@ -172,8 +224,18 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
172
224
|
const ctxPctColor = chalk[item.color || "cyan"] || chalk.cyan;
|
|
173
225
|
elements.push(ctxPctColor("Ctx: 9.3%"));
|
|
174
226
|
break;
|
|
227
|
+
case "session-clock":
|
|
228
|
+
const sessionColor = chalk[item.color || "blue"] || chalk.blue;
|
|
229
|
+
elements.push(sessionColor("Session: 2hr 15m"));
|
|
230
|
+
break;
|
|
231
|
+
case "terminal-width":
|
|
232
|
+
const termColor = chalk[item.color || "dim"] || chalk.dim;
|
|
233
|
+
const detectedWidth = canDetectTerminalWidth() ? terminalWidth : "??";
|
|
234
|
+
elements.push(termColor(`Term: ${detectedWidth}`));
|
|
235
|
+
break;
|
|
175
236
|
case "separator":
|
|
176
|
-
|
|
237
|
+
const sepChar = item.character || "|";
|
|
238
|
+
elements.push(chalk.dim(` ${sepChar} `));
|
|
177
239
|
break;
|
|
178
240
|
case "flex-separator":
|
|
179
241
|
elements.push("FLEX");
|
|
@@ -182,7 +244,7 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
182
244
|
}
|
|
183
245
|
});
|
|
184
246
|
let statusLine = "";
|
|
185
|
-
if (hasFlexSeparator) {
|
|
247
|
+
if (hasFlexSeparator && width) {
|
|
186
248
|
const parts = [[]];
|
|
187
249
|
let currentPart = 0;
|
|
188
250
|
for (let i = 0;i < items.length; i++) {
|
|
@@ -218,12 +280,17 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
218
280
|
}
|
|
219
281
|
}
|
|
220
282
|
} else {
|
|
221
|
-
statusLine = elements.
|
|
283
|
+
statusLine = elements.map((e) => e === "FLEX" ? chalk.dim(" | ") : e).join("");
|
|
222
284
|
}
|
|
285
|
+
return statusLine;
|
|
286
|
+
};
|
|
287
|
+
var StatusLinePreview = ({ lines, terminalWidth }) => {
|
|
288
|
+
const widthDetectionAvailable = canDetectTerminalWidth();
|
|
223
289
|
const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
|
|
224
290
|
const topLine = chalk.dim("╭" + "─".repeat(Math.max(0, boxWidth - 2)) + "╮");
|
|
225
291
|
const middleLine = chalk.dim("│") + " > " + " ".repeat(Math.max(0, boxWidth - 5)) + chalk.dim("│");
|
|
226
292
|
const bottomLine = chalk.dim("╰" + "─".repeat(Math.max(0, boxWidth - 2)) + "╯");
|
|
293
|
+
const renderedLines = lines.map((lineItems) => lineItems.length > 0 ? renderSingleLine(lineItems, boxWidth, widthDetectionAvailable) : "").filter((line) => line !== "");
|
|
227
294
|
return /* @__PURE__ */ jsxDEV(Box, {
|
|
228
295
|
flexDirection: "column",
|
|
229
296
|
children: [
|
|
@@ -236,9 +303,9 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
236
303
|
/* @__PURE__ */ jsxDEV(Text, {
|
|
237
304
|
children: bottomLine
|
|
238
305
|
}, undefined, false, undefined, this),
|
|
239
|
-
/* @__PURE__ */ jsxDEV(Text, {
|
|
240
|
-
children:
|
|
241
|
-
},
|
|
306
|
+
renderedLines.map((line, index) => /* @__PURE__ */ jsxDEV(Text, {
|
|
307
|
+
children: line
|
|
308
|
+
}, index, false, undefined, this))
|
|
242
309
|
]
|
|
243
310
|
}, undefined, true, undefined, this);
|
|
244
311
|
};
|
|
@@ -263,9 +330,53 @@ var ConfirmDialog = ({ message, onConfirm, onCancel }) => {
|
|
|
263
330
|
]
|
|
264
331
|
}, undefined, true, undefined, this);
|
|
265
332
|
};
|
|
333
|
+
var LineSelector = ({ lines, onSelect, onBack }) => {
|
|
334
|
+
const items = [
|
|
335
|
+
{ label: `\uD83D\uDCDD Line 1${lines[0] && lines[0].length > 0 ? ` (${lines[0].length} items)` : " (empty)"}`, value: 0 },
|
|
336
|
+
{ label: `\uD83D\uDCDD Line 2${lines[1] && lines[1].length > 0 ? ` (${lines[1].length} items)` : " (empty)"}`, value: 1 },
|
|
337
|
+
{ label: `\uD83D\uDCDD Line 3${lines[2] && lines[2].length > 0 ? ` (${lines[2].length} items)` : " (empty)"}`, value: 2 },
|
|
338
|
+
{ label: "← Back", value: -1 }
|
|
339
|
+
];
|
|
340
|
+
const handleSelect = (item) => {
|
|
341
|
+
if (item.value === -1) {
|
|
342
|
+
onBack();
|
|
343
|
+
} else {
|
|
344
|
+
onSelect(item.value);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
useInput((input, key) => {
|
|
348
|
+
if (key.escape) {
|
|
349
|
+
onBack();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
353
|
+
flexDirection: "column",
|
|
354
|
+
children: [
|
|
355
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
356
|
+
bold: true,
|
|
357
|
+
children: "Select Line to Edit"
|
|
358
|
+
}, undefined, false, undefined, this),
|
|
359
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
360
|
+
dimColor: true,
|
|
361
|
+
children: "Choose which status line to configure (up to 3 lines supported)"
|
|
362
|
+
}, undefined, false, undefined, this),
|
|
363
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
364
|
+
dimColor: true,
|
|
365
|
+
children: "Press ESC to go back"
|
|
366
|
+
}, undefined, false, undefined, this),
|
|
367
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
368
|
+
marginTop: 1,
|
|
369
|
+
children: /* @__PURE__ */ jsxDEV(SelectInput, {
|
|
370
|
+
items,
|
|
371
|
+
onSelect: handleSelect
|
|
372
|
+
}, undefined, false, undefined, this)
|
|
373
|
+
}, undefined, false, undefined, this)
|
|
374
|
+
]
|
|
375
|
+
}, undefined, true, undefined, this);
|
|
376
|
+
};
|
|
266
377
|
var MainMenu = ({ onSelect, isClaudeInstalled, hasChanges }) => {
|
|
267
378
|
const items = [
|
|
268
|
-
{ label: "\uD83D\uDCDD Edit
|
|
379
|
+
{ label: "\uD83D\uDCDD Edit Lines", value: "lines" },
|
|
269
380
|
{ label: "\uD83C\uDFA8 Configure Colors", value: "colors" },
|
|
270
381
|
{ label: isClaudeInstalled ? "\uD83D\uDDD1️ Uninstall from Claude Code" : "\uD83D\uDCE6 Install to Claude Code", value: "install" }
|
|
271
382
|
];
|
|
@@ -291,9 +402,10 @@ var MainMenu = ({ onSelect, isClaudeInstalled, hasChanges }) => {
|
|
|
291
402
|
]
|
|
292
403
|
}, undefined, true, undefined, this);
|
|
293
404
|
};
|
|
294
|
-
var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
405
|
+
var ItemsEditor = ({ items, onUpdate, onBack, lineNumber }) => {
|
|
295
406
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
296
407
|
const [moveMode, setMoveMode] = useState(false);
|
|
408
|
+
const separatorChars = ["|", "-", " "];
|
|
297
409
|
useInput((input, key) => {
|
|
298
410
|
if (moveMode) {
|
|
299
411
|
if (key.upArrow && selectedIndex > 0) {
|
|
@@ -328,13 +440,15 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
328
440
|
"git-branch",
|
|
329
441
|
"git-changes",
|
|
330
442
|
"separator",
|
|
331
|
-
"flex-separator",
|
|
332
443
|
"tokens-input",
|
|
333
444
|
"tokens-output",
|
|
334
445
|
"tokens-cached",
|
|
335
446
|
"tokens-total",
|
|
336
447
|
"context-length",
|
|
337
|
-
"context-percentage"
|
|
448
|
+
"context-percentage",
|
|
449
|
+
"session-clock",
|
|
450
|
+
"terminal-width",
|
|
451
|
+
"flex-separator"
|
|
338
452
|
];
|
|
339
453
|
const currentItem = items[selectedIndex];
|
|
340
454
|
if (currentItem) {
|
|
@@ -354,13 +468,15 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
354
468
|
"git-branch",
|
|
355
469
|
"git-changes",
|
|
356
470
|
"separator",
|
|
357
|
-
"flex-separator",
|
|
358
471
|
"tokens-input",
|
|
359
472
|
"tokens-output",
|
|
360
473
|
"tokens-cached",
|
|
361
474
|
"tokens-total",
|
|
362
475
|
"context-length",
|
|
363
|
-
"context-percentage"
|
|
476
|
+
"context-percentage",
|
|
477
|
+
"session-clock",
|
|
478
|
+
"terminal-width",
|
|
479
|
+
"flex-separator"
|
|
364
480
|
];
|
|
365
481
|
const currentItem = items[selectedIndex];
|
|
366
482
|
if (currentItem) {
|
|
@@ -381,7 +497,9 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
381
497
|
id: Date.now().toString(),
|
|
382
498
|
type: "separator"
|
|
383
499
|
};
|
|
384
|
-
|
|
500
|
+
const newItems = [...items, newItem];
|
|
501
|
+
onUpdate(newItems);
|
|
502
|
+
setSelectedIndex(newItems.length - 1);
|
|
385
503
|
} else if (input === "i") {
|
|
386
504
|
const newItem = {
|
|
387
505
|
id: Date.now().toString(),
|
|
@@ -397,6 +515,19 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
397
515
|
if (selectedIndex >= newItems.length && selectedIndex > 0) {
|
|
398
516
|
setSelectedIndex(selectedIndex - 1);
|
|
399
517
|
}
|
|
518
|
+
} else if (input === "c") {
|
|
519
|
+
onUpdate([]);
|
|
520
|
+
setSelectedIndex(0);
|
|
521
|
+
} else if (input === " " && items.length > 0) {
|
|
522
|
+
const currentItem = items[selectedIndex];
|
|
523
|
+
if (currentItem && currentItem.type === "separator") {
|
|
524
|
+
const currentChar = currentItem.character || "|";
|
|
525
|
+
const currentCharIndex = separatorChars.indexOf(currentChar);
|
|
526
|
+
const nextChar = separatorChars[(currentCharIndex + 1) % separatorChars.length];
|
|
527
|
+
const newItems = [...items];
|
|
528
|
+
newItems[selectedIndex] = { ...currentItem, character: nextChar };
|
|
529
|
+
onUpdate(newItems);
|
|
530
|
+
}
|
|
400
531
|
} else if (key.escape) {
|
|
401
532
|
onBack();
|
|
402
533
|
}
|
|
@@ -410,10 +541,13 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
410
541
|
return chalk.magenta("Git Branch");
|
|
411
542
|
case "git-changes":
|
|
412
543
|
return chalk.yellow("Git Changes");
|
|
413
|
-
case "separator":
|
|
414
|
-
|
|
544
|
+
case "separator": {
|
|
545
|
+
const char = item.character || "|";
|
|
546
|
+
const charDisplay = char === " " ? "(space)" : char;
|
|
547
|
+
return chalk.dim(`Separator ${charDisplay}`);
|
|
548
|
+
}
|
|
415
549
|
case "flex-separator":
|
|
416
|
-
return chalk.yellow("Flex Separator
|
|
550
|
+
return chalk.yellow("Flex Separator");
|
|
417
551
|
case "tokens-input":
|
|
418
552
|
return chalk.yellow("Tokens Input");
|
|
419
553
|
case "tokens-output":
|
|
@@ -426,15 +560,23 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
426
560
|
return chalk.cyan("Context Length");
|
|
427
561
|
case "context-percentage":
|
|
428
562
|
return chalk.cyan("Context %");
|
|
563
|
+
case "session-clock":
|
|
564
|
+
return chalk.blue("Session Clock");
|
|
565
|
+
case "terminal-width":
|
|
566
|
+
return chalk.dim("Terminal Width");
|
|
429
567
|
}
|
|
430
568
|
};
|
|
569
|
+
const hasFlexSeparator = items.some((item) => item.type === "flex-separator");
|
|
570
|
+
const widthDetectionAvailable = canDetectTerminalWidth();
|
|
431
571
|
return /* @__PURE__ */ jsxDEV(Box, {
|
|
432
572
|
flexDirection: "column",
|
|
433
573
|
children: [
|
|
434
574
|
/* @__PURE__ */ jsxDEV(Text, {
|
|
435
575
|
bold: true,
|
|
436
576
|
children: [
|
|
437
|
-
"Edit
|
|
577
|
+
"Edit Line ",
|
|
578
|
+
lineNumber,
|
|
579
|
+
" ",
|
|
438
580
|
moveMode && /* @__PURE__ */ jsxDEV(Text, {
|
|
439
581
|
color: "yellow",
|
|
440
582
|
children: "[MOVE MODE]"
|
|
@@ -446,8 +588,21 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
446
588
|
children: "↑↓ to move item, ESC or Enter to exit move mode"
|
|
447
589
|
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Text, {
|
|
448
590
|
dimColor: true,
|
|
449
|
-
children: "↑↓ select, ←→ change type, Enter to move, (a)dd, (i)nsert, (d)elete, ESC back"
|
|
591
|
+
children: "↑↓ select, ←→ change type, Space edit separator, Enter to move, (a)dd, (i)nsert, (d)elete, (c)lear line, ESC back"
|
|
450
592
|
}, undefined, false, undefined, this),
|
|
593
|
+
hasFlexSeparator && !widthDetectionAvailable && /* @__PURE__ */ jsxDEV(Box, {
|
|
594
|
+
marginTop: 1,
|
|
595
|
+
children: [
|
|
596
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
597
|
+
color: "yellow",
|
|
598
|
+
children: "⚠ Note: Terminal width detection is currently unavailable in your environment."
|
|
599
|
+
}, undefined, false, undefined, this),
|
|
600
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
601
|
+
dimColor: true,
|
|
602
|
+
children: " Flex separators will act as normal separators until width detection is available."
|
|
603
|
+
}, undefined, false, undefined, this)
|
|
604
|
+
]
|
|
605
|
+
}, undefined, true, undefined, this),
|
|
451
606
|
/* @__PURE__ */ jsxDEV(Box, {
|
|
452
607
|
marginTop: 1,
|
|
453
608
|
flexDirection: "column",
|
|
@@ -461,7 +616,11 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
461
616
|
index === selectedIndex ? moveMode ? "◆ " : "▶ " : " ",
|
|
462
617
|
index + 1,
|
|
463
618
|
". ",
|
|
464
|
-
getItemDisplay(item)
|
|
619
|
+
getItemDisplay(item),
|
|
620
|
+
item.type === "separator" && index === selectedIndex && !moveMode && /* @__PURE__ */ jsxDEV(Text, {
|
|
621
|
+
dimColor: true,
|
|
622
|
+
children: " (Space to edit)"
|
|
623
|
+
}, undefined, false, undefined, this)
|
|
465
624
|
]
|
|
466
625
|
}, undefined, true, undefined, this)
|
|
467
626
|
}, item.id, false, undefined, this))
|
|
@@ -470,7 +629,7 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
470
629
|
}, undefined, true, undefined, this);
|
|
471
630
|
};
|
|
472
631
|
var ColorMenu = ({ items, onUpdate, onBack }) => {
|
|
473
|
-
const colorableItems = items.filter((item) => ["model", "git-branch", "tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage"].includes(item.type));
|
|
632
|
+
const colorableItems = items.filter((item) => ["model", "git-branch", "tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage", "session-clock"].includes(item.type));
|
|
474
633
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
475
634
|
useInput((input, key) => {
|
|
476
635
|
if (key.escape) {
|
|
@@ -531,6 +690,8 @@ var ColorMenu = ({ items, onUpdate, onBack }) => {
|
|
|
531
690
|
return "Context Length";
|
|
532
691
|
case "context-percentage":
|
|
533
692
|
return "Context Percentage";
|
|
693
|
+
case "session-clock":
|
|
694
|
+
return "Session Clock";
|
|
534
695
|
default:
|
|
535
696
|
return item.type;
|
|
536
697
|
}
|
|
@@ -632,11 +793,18 @@ var App = () => {
|
|
|
632
793
|
const [originalSettings, setOriginalSettings] = useState(null);
|
|
633
794
|
const [hasChanges, setHasChanges] = useState(false);
|
|
634
795
|
const [screen, setScreen] = useState("main");
|
|
796
|
+
const [selectedLine, setSelectedLine] = useState(0);
|
|
635
797
|
const [confirmDialog, setConfirmDialog] = useState(null);
|
|
636
798
|
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
|
|
637
799
|
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
638
800
|
useEffect(() => {
|
|
639
801
|
loadSettings().then((loadedSettings) => {
|
|
802
|
+
if (!loadedSettings.lines) {
|
|
803
|
+
loadedSettings.lines = [[]];
|
|
804
|
+
}
|
|
805
|
+
while (loadedSettings.lines.length < 3) {
|
|
806
|
+
loadedSettings.lines.push([]);
|
|
807
|
+
}
|
|
640
808
|
setSettings(loadedSettings);
|
|
641
809
|
setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings)));
|
|
642
810
|
});
|
|
@@ -687,7 +855,7 @@ A status line is already configured: "${existing}"
|
|
|
687
855
|
Replace it with npx -y ccstatusline@latest?`;
|
|
688
856
|
} else if (existing === "npx -y ccstatusline@latest") {
|
|
689
857
|
message = `ccstatusline is already installed in ~/.claude/settings.json
|
|
690
|
-
|
|
858
|
+
Update it with the latest options?`;
|
|
691
859
|
} else {
|
|
692
860
|
message = `This will modify ~/.claude/settings.json to add ccstatusline.
|
|
693
861
|
Continue?`;
|
|
@@ -706,8 +874,8 @@ Continue?`;
|
|
|
706
874
|
};
|
|
707
875
|
const handleMainMenuSelect = async (value) => {
|
|
708
876
|
switch (value) {
|
|
709
|
-
case "
|
|
710
|
-
setScreen("
|
|
877
|
+
case "lines":
|
|
878
|
+
setScreen("lines");
|
|
711
879
|
break;
|
|
712
880
|
case "colors":
|
|
713
881
|
setScreen("colors");
|
|
@@ -726,8 +894,14 @@ Continue?`;
|
|
|
726
894
|
break;
|
|
727
895
|
}
|
|
728
896
|
};
|
|
729
|
-
const
|
|
730
|
-
|
|
897
|
+
const updateLine = (lineIndex, items) => {
|
|
898
|
+
const newLines = [...settings.lines || []];
|
|
899
|
+
newLines[lineIndex] = items;
|
|
900
|
+
setSettings({ ...settings, lines: newLines });
|
|
901
|
+
};
|
|
902
|
+
const handleLineSelect = (lineIndex) => {
|
|
903
|
+
setSelectedLine(lineIndex);
|
|
904
|
+
setScreen("items");
|
|
731
905
|
};
|
|
732
906
|
return /* @__PURE__ */ jsxDEV(Box, {
|
|
733
907
|
flexDirection: "column",
|
|
@@ -749,7 +923,7 @@ Continue?`;
|
|
|
749
923
|
}, undefined, false, undefined, this)
|
|
750
924
|
}, undefined, false, undefined, this),
|
|
751
925
|
/* @__PURE__ */ jsxDEV(StatusLinePreview, {
|
|
752
|
-
|
|
926
|
+
lines: settings.lines || [[]],
|
|
753
927
|
terminalWidth
|
|
754
928
|
}, undefined, false, undefined, this),
|
|
755
929
|
/* @__PURE__ */ jsxDEV(Box, {
|
|
@@ -760,14 +934,32 @@ Continue?`;
|
|
|
760
934
|
isClaudeInstalled,
|
|
761
935
|
hasChanges
|
|
762
936
|
}, undefined, false, undefined, this),
|
|
763
|
-
screen === "
|
|
764
|
-
|
|
765
|
-
|
|
937
|
+
screen === "lines" && /* @__PURE__ */ jsxDEV(LineSelector, {
|
|
938
|
+
lines: settings.lines || [[]],
|
|
939
|
+
onSelect: handleLineSelect,
|
|
766
940
|
onBack: () => setScreen("main")
|
|
767
941
|
}, undefined, false, undefined, this),
|
|
942
|
+
screen === "items" && settings.lines && /* @__PURE__ */ jsxDEV(ItemsEditor, {
|
|
943
|
+
items: settings.lines[selectedLine] || [],
|
|
944
|
+
onUpdate: (items) => updateLine(selectedLine, items),
|
|
945
|
+
onBack: () => setScreen("lines"),
|
|
946
|
+
lineNumber: selectedLine + 1
|
|
947
|
+
}, undefined, false, undefined, this),
|
|
768
948
|
screen === "colors" && /* @__PURE__ */ jsxDEV(ColorMenu, {
|
|
769
|
-
items: settings.
|
|
770
|
-
onUpdate:
|
|
949
|
+
items: settings.lines?.flat() || [],
|
|
950
|
+
onUpdate: (items) => {
|
|
951
|
+
const newLines = settings.lines || [[]];
|
|
952
|
+
let flatIndex = 0;
|
|
953
|
+
for (let lineIndex = 0;lineIndex < newLines.length; lineIndex++) {
|
|
954
|
+
for (let itemIndex = 0;itemIndex < newLines[lineIndex].length; itemIndex++) {
|
|
955
|
+
if (flatIndex < items.length) {
|
|
956
|
+
newLines[lineIndex][itemIndex] = items[flatIndex];
|
|
957
|
+
flatIndex++;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
setSettings({ ...settings, lines: newLines });
|
|
962
|
+
},
|
|
771
963
|
onBack: () => setScreen("main")
|
|
772
964
|
}, undefined, false, undefined, this),
|
|
773
965
|
screen === "confirm" && confirmDialog && /* @__PURE__ */ jsxDEV(ConfirmDialog, {
|
|
@@ -814,9 +1006,40 @@ async function readStdin() {
|
|
|
814
1006
|
return null;
|
|
815
1007
|
}
|
|
816
1008
|
}
|
|
1009
|
+
function getTerminalWidth() {
|
|
1010
|
+
try {
|
|
1011
|
+
const tty = execSync2("ps -o tty= -p $(ps -o ppid= -p $$)", {
|
|
1012
|
+
encoding: "utf8",
|
|
1013
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1014
|
+
shell: "/bin/sh"
|
|
1015
|
+
}).trim();
|
|
1016
|
+
if (tty && tty !== "??" && tty !== "?") {
|
|
1017
|
+
const width = execSync2(`stty size < /dev/${tty} | awk '{print $2}'`, {
|
|
1018
|
+
encoding: "utf8",
|
|
1019
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1020
|
+
shell: "/bin/sh"
|
|
1021
|
+
}).trim();
|
|
1022
|
+
const parsed = parseInt(width, 10);
|
|
1023
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1024
|
+
return parsed;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} catch {}
|
|
1028
|
+
try {
|
|
1029
|
+
const width = execSync2("tput cols 2>/dev/null", {
|
|
1030
|
+
encoding: "utf8",
|
|
1031
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1032
|
+
}).trim();
|
|
1033
|
+
const parsed = parseInt(width, 10);
|
|
1034
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1035
|
+
return parsed;
|
|
1036
|
+
}
|
|
1037
|
+
} catch {}
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
817
1040
|
function getGitBranch() {
|
|
818
1041
|
try {
|
|
819
|
-
const branch =
|
|
1042
|
+
const branch = execSync2("git branch --show-current 2>/dev/null", {
|
|
820
1043
|
encoding: "utf8",
|
|
821
1044
|
stdio: ["pipe", "pipe", "ignore"]
|
|
822
1045
|
}).trim();
|
|
@@ -829,11 +1052,11 @@ function getGitChanges() {
|
|
|
829
1052
|
try {
|
|
830
1053
|
let totalInsertions = 0;
|
|
831
1054
|
let totalDeletions = 0;
|
|
832
|
-
const unstagedStat =
|
|
1055
|
+
const unstagedStat = execSync2("git diff --shortstat 2>/dev/null", {
|
|
833
1056
|
encoding: "utf8",
|
|
834
1057
|
stdio: ["pipe", "pipe", "ignore"]
|
|
835
1058
|
}).trim();
|
|
836
|
-
const stagedStat =
|
|
1059
|
+
const stagedStat = execSync2("git diff --cached --shortstat 2>/dev/null", {
|
|
837
1060
|
encoding: "utf8",
|
|
838
1061
|
stdio: ["pipe", "pipe", "ignore"]
|
|
839
1062
|
}).trim();
|
|
@@ -857,6 +1080,58 @@ function getGitChanges() {
|
|
|
857
1080
|
return null;
|
|
858
1081
|
}
|
|
859
1082
|
}
|
|
1083
|
+
async function getSessionDuration(transcriptPath) {
|
|
1084
|
+
try {
|
|
1085
|
+
if (!fs3.existsSync(transcriptPath)) {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const content = await readFile6(transcriptPath, "utf-8");
|
|
1089
|
+
const lines = content.trim().split(`
|
|
1090
|
+
`).filter((line) => line.trim());
|
|
1091
|
+
if (lines.length === 0) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
let firstTimestamp = null;
|
|
1095
|
+
let lastTimestamp = null;
|
|
1096
|
+
for (const line of lines) {
|
|
1097
|
+
try {
|
|
1098
|
+
const data = JSON.parse(line);
|
|
1099
|
+
if (data.timestamp) {
|
|
1100
|
+
firstTimestamp = new Date(data.timestamp);
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
} catch {}
|
|
1104
|
+
}
|
|
1105
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
1106
|
+
try {
|
|
1107
|
+
const data = JSON.parse(lines[i]);
|
|
1108
|
+
if (data.timestamp) {
|
|
1109
|
+
lastTimestamp = new Date(data.timestamp);
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
} catch {}
|
|
1113
|
+
}
|
|
1114
|
+
if (!firstTimestamp || !lastTimestamp) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
|
|
1118
|
+
const totalMinutes = Math.floor(durationMs / (1000 * 60));
|
|
1119
|
+
if (totalMinutes < 1) {
|
|
1120
|
+
return "<1m";
|
|
1121
|
+
}
|
|
1122
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1123
|
+
const minutes = totalMinutes % 60;
|
|
1124
|
+
if (hours === 0) {
|
|
1125
|
+
return `${minutes}m`;
|
|
1126
|
+
} else if (minutes === 0) {
|
|
1127
|
+
return `${hours}hr`;
|
|
1128
|
+
} else {
|
|
1129
|
+
return `${hours}hr ${minutes}m`;
|
|
1130
|
+
}
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
860
1135
|
async function getTokenMetrics(transcriptPath) {
|
|
861
1136
|
try {
|
|
862
1137
|
if (!fs3.existsSync(transcriptPath)) {
|
|
@@ -892,16 +1167,11 @@ async function getTokenMetrics(transcriptPath) {
|
|
|
892
1167
|
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
|
893
1168
|
}
|
|
894
1169
|
}
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
const terminalWidth =
|
|
1170
|
+
function renderSingleLine2(items, settings, data, tokenMetrics, sessionDuration) {
|
|
1171
|
+
const detectedWidth = getTerminalWidth();
|
|
1172
|
+
const terminalWidth = detectedWidth ? detectedWidth - 40 : null;
|
|
898
1173
|
const elements = [];
|
|
899
1174
|
let hasFlexSeparator = false;
|
|
900
|
-
const hasTokenItems = settings.items.some((item) => ["tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage"].includes(item.type));
|
|
901
|
-
let tokenMetrics = null;
|
|
902
|
-
if (hasTokenItems && data.transcript_path) {
|
|
903
|
-
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
|
904
|
-
}
|
|
905
1175
|
const formatTokens = (count) => {
|
|
906
1176
|
if (count >= 1e6)
|
|
907
1177
|
return `${(count / 1e6).toFixed(1)}M`;
|
|
@@ -909,7 +1179,7 @@ async function renderStatusLine(data) {
|
|
|
909
1179
|
return `${(count / 1000).toFixed(1)}k`;
|
|
910
1180
|
return count.toString();
|
|
911
1181
|
};
|
|
912
|
-
for (const item of
|
|
1182
|
+
for (const item of items) {
|
|
913
1183
|
switch (item.type) {
|
|
914
1184
|
case "model":
|
|
915
1185
|
if (data.model) {
|
|
@@ -969,11 +1239,25 @@ async function renderStatusLine(data) {
|
|
|
969
1239
|
elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: "context-percentage" });
|
|
970
1240
|
}
|
|
971
1241
|
break;
|
|
1242
|
+
case "terminal-width":
|
|
1243
|
+
const detectedWidth2 = terminalWidth || getTerminalWidth();
|
|
1244
|
+
if (detectedWidth2) {
|
|
1245
|
+
const color = chalk2[item.color || "dim"] || chalk2.dim;
|
|
1246
|
+
elements.push({ content: color(`Term: ${detectedWidth2}`), type: "terminal-width" });
|
|
1247
|
+
}
|
|
1248
|
+
break;
|
|
1249
|
+
case "session-clock":
|
|
1250
|
+
if (sessionDuration) {
|
|
1251
|
+
const color = chalk2[item.color || "blue"] || chalk2.blue;
|
|
1252
|
+
elements.push({ content: color(`Session: ${sessionDuration}`), type: "session-clock" });
|
|
1253
|
+
}
|
|
1254
|
+
break;
|
|
972
1255
|
case "separator":
|
|
973
1256
|
const lastElement = elements[elements.length - 1];
|
|
974
1257
|
if (elements.length > 0 && lastElement && lastElement.type !== "separator") {
|
|
975
1258
|
const sepColor = chalk2[settings.colors.separator] || chalk2.dim;
|
|
976
|
-
|
|
1259
|
+
const sepChar = item.character || "|";
|
|
1260
|
+
elements.push({ content: sepColor(` ${sepChar} `), type: "separator" });
|
|
977
1261
|
}
|
|
978
1262
|
break;
|
|
979
1263
|
case "flex-separator":
|
|
@@ -983,9 +1267,9 @@ async function renderStatusLine(data) {
|
|
|
983
1267
|
}
|
|
984
1268
|
}
|
|
985
1269
|
if (elements.length === 0)
|
|
986
|
-
return;
|
|
1270
|
+
return "";
|
|
987
1271
|
let statusLine = "";
|
|
988
|
-
if (hasFlexSeparator) {
|
|
1272
|
+
if (hasFlexSeparator && terminalWidth) {
|
|
989
1273
|
const parts = [[]];
|
|
990
1274
|
let currentPart = 0;
|
|
991
1275
|
for (const elem of elements) {
|
|
@@ -1017,20 +1301,39 @@ async function renderStatusLine(data) {
|
|
|
1017
1301
|
}
|
|
1018
1302
|
}
|
|
1019
1303
|
} else {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
statusLine = statusLine + " ".repeat(remainingSpace);
|
|
1304
|
+
if (hasFlexSeparator && !terminalWidth) {
|
|
1305
|
+
statusLine = elements.map((e) => e.type === "flex-separator" ? chalk2.dim(" | ") : e.content).join("");
|
|
1306
|
+
} else {
|
|
1307
|
+
statusLine = elements.map((e) => e.content).join("");
|
|
1025
1308
|
}
|
|
1026
1309
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1310
|
+
return statusLine;
|
|
1311
|
+
}
|
|
1312
|
+
async function renderStatusLine(data) {
|
|
1313
|
+
const settings = await loadSettings();
|
|
1314
|
+
let lines = [];
|
|
1315
|
+
if (settings.lines) {
|
|
1316
|
+
lines = settings.lines;
|
|
1317
|
+
} else if (settings.items) {
|
|
1318
|
+
lines = [settings.items];
|
|
1032
1319
|
} else {
|
|
1033
|
-
|
|
1320
|
+
lines = [[]];
|
|
1321
|
+
}
|
|
1322
|
+
const hasTokenItems = lines.some((line) => line.some((item) => ["tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage"].includes(item.type)));
|
|
1323
|
+
const hasSessionClock = lines.some((line) => line.some((item) => item.type === "session-clock"));
|
|
1324
|
+
let tokenMetrics = null;
|
|
1325
|
+
if (hasTokenItems && data.transcript_path) {
|
|
1326
|
+
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
|
1327
|
+
}
|
|
1328
|
+
let sessionDuration = null;
|
|
1329
|
+
if (hasSessionClock && data.transcript_path) {
|
|
1330
|
+
sessionDuration = await getSessionDuration(data.transcript_path);
|
|
1331
|
+
}
|
|
1332
|
+
for (const lineItems of lines) {
|
|
1333
|
+
if (lineItems.length > 0) {
|
|
1334
|
+
const line = renderSingleLine2(lineItems, settings, data, tokenMetrics, sessionDuration);
|
|
1335
|
+
console.log(line);
|
|
1336
|
+
}
|
|
1034
1337
|
}
|
|
1035
1338
|
}
|
|
1036
1339
|
async function main() {
|