lumira 1.14.0 → 1.15.1

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Real-time statusline HUD for Claude Code and Qwen Code — analytics, quota projection, themes, powerline",
8
- "version": "1.14.0"
8
+ "version": "1.15.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "lumira",
13
13
  "source": "./",
14
14
  "description": "Real-time statusline HUD for Claude Code and Qwen Code. Session analytics, API latency widget, 7-day quota projection, auto-compact warnings, 7 themes, powerline support. Zero runtime dependencies. Run /lumira:setup after install to activate.",
15
- "version": "1.14.0",
15
+ "version": "1.15.1",
16
16
  "author": {
17
17
  "name": "Carlos Cativo"
18
18
  },
@@ -32,5 +32,5 @@
32
32
  ]
33
33
  }
34
34
  ],
35
- "version": "1.14.0"
35
+ "version": "1.15.1"
36
36
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "description": "Real-time statusline HUD for Claude Code and Qwen Code — session analytics, API latency, 7-day quota projection, auto-compact warnings, 7 themes, powerline. Zero runtime deps.",
5
5
  "author": {
6
6
  "name": "Carlos Cativo"
package/README.md CHANGED
@@ -35,7 +35,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
35
35
  ![Claude Code](https://img.shields.io/badge/Claude_Code-compatible-2d3748?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PHBhdGggZD0iTTY0IDEyOEMzNS44IDEyOCAxMyAxMDUuMiAxMyA3N0MxMyA0OC44IDM1LjggMjYgNjQgMjZjMjguMiAwIDUxIDIyLjggNTEgNTFzLTIyLjggNTEtNTEgNTF6IiBmaWxsPSIjMjQyNTJGIi8+PC9zdmc+)
36
36
  ![Qwen Code](https://img.shields.io/badge/Qwen_Code-compatible-6156FF)
37
37
 
38
- > **What's new in v1.9.0:** lumira is now a **Claude Code plugin** install with `/plugin marketplace add cativo23/lumira`, no npm required. Run `/lumira:setup` to activate. v1.8.2 made the installer write a fast per-render command (~10× faster). v1.8.1 brought the GSD widget to parity with GSD 1.42.3. Earlier: compaction counter `⊙ N` (v1.8.0), added-dirs badge + worktree breadcrumb (v1.7.0), [`lumira stats` CLI](#stats-cli) (v1.5), `API N%` latency widget (v1.4.0), 7-day quota projection (v1.3.0).
38
+ > **What's new in v1.15.0:** `line1Align: "packed"` packs line 1 tightly to the left with only the version pinned to the true right edge; the repo segment now sits next to the branch (git identity grouped, `branch repo → directory`); and a Box Drawing width fix (the `│` separator was miscounted) aligns every line to the real terminal edge. Recent highlights: [subagent panel rows](#subagent-rows) (v1.14), extended-thinking badge (v1.12), [PR widget](#features) (v1.11), repo-identity segment + a full [feature comparison](#how-lumira-compares) (v1.10–v1.13). Still a **Claude Code plugin** — `/plugin marketplace add cativo23/lumira`, no npm required; run `/lumira:setup` to activate.
39
39
 
40
40
  ## Table of contents
41
41
 
@@ -321,6 +321,7 @@ Create `~/.config/lumira/config.json`:
321
321
  "theme": "tokyo-night",
322
322
  "icons": "nerd",
323
323
  "style": "classic",
324
+ "line1Align": "justified",
324
325
  "powerline": { "style": "auto" },
325
326
  "gsd": false,
326
327
  "colors": { "mode": "auto" },
@@ -364,7 +365,7 @@ Create `~/.config/lumira/config.json`:
364
365
  }
365
366
  ```
366
367
 
367
- All fields are optional — defaults are shown above. `display.health` defaults to `false` (opt-in widget).
368
+ All fields are optional — defaults are shown above. `display.health` defaults to `false` (opt-in widget). `line1Align` controls line 1's classic layout: `"justified"` (default) pins the left cluster to the start and the right cluster to the end; `"packed"` packs every segment tightly to the left with no forced middle gap, except the app version string, which stays pinned to the true right edge (the same idiom line 2 uses for its small right-anchored indicators). On a terminal too narrow to fit the version alongside the packed content, the version disappears entirely rather than truncating — an intentional all-or-nothing tradeoff. Powerline mode always packs.
368
369
 
369
370
  **Context bar thresholds** — `contextWarningThreshold` (default 70) and `contextCriticalThreshold` (default 85) control when the bar transitions through yellow/orange/red. Both are clamped to `[0, 100]` and `warning < critical` is required (invalid pairs fall back to defaults with a one-shot stderr warning). Lower them for earlier warnings, raise them if your workflow tolerates fuller buffers.
370
371
 
@@ -486,10 +487,11 @@ PRs welcome — particularly for new themes (one of the most common contribution
486
487
 
487
488
  ### What's next
488
489
 
489
- - **v1.0** — soak window on v0.7.x, then tagging stable. CLI flags, preset names, and config schema are considered frozen from this point.
490
490
  - **Themes** — community theme contributions welcome via the theme PR template.
491
491
  - **Backlog** — incremental transcript parsing for very large sessions (deferred; full re-parse stays under budget for real-world transcripts).
492
492
 
493
+ CLI flags, preset names, and the config schema have been stable since v1.0 — additive changes only.
494
+
493
495
  For security issues, see [SECURITY.md](SECURITY.md).
494
496
 
495
497
  ## Credits
package/dist/config.js CHANGED
@@ -196,6 +196,7 @@ function mergeConfig(rawIn) {
196
196
  raw = { ...raw, preset: 'minimal' };
197
197
  }
198
198
  const layout = ['multiline', 'singleline', 'auto'].includes(raw.layout) ? raw.layout : DEFAULT_CONFIG.layout;
199
+ const line1Align = ['justified', 'packed'].includes(raw.line1Align) ? raw.line1Align : DEFAULT_CONFIG.line1Align;
199
200
  const colors = { ...DEFAULT_CONFIG.colors };
200
201
  if (raw.colors && typeof raw.colors === 'object') {
201
202
  const m = raw.colors.mode;
@@ -204,6 +205,7 @@ function mergeConfig(rawIn) {
204
205
  }
205
206
  const result = {
206
207
  layout,
208
+ line1Align,
207
209
  gsd: typeof raw.gsd === 'boolean' ? raw.gsd : DEFAULT_CONFIG.gsd,
208
210
  display: { ...DEFAULT_DISPLAY },
209
211
  colors,
@@ -6,7 +6,7 @@ import { hyperlink } from './hyperlink.js';
6
6
  import { formatDuration } from '../utils/format.js';
7
7
  import { isNamedAgentType } from '../parsers/subagents.js';
8
8
  export function renderLine1(ctx, c) {
9
- const { input, git, transcript, config: { display }, cols, icons, memory, tokenSpeed } = ctx;
9
+ const { input, git, transcript, config: { display, line1Align }, cols, icons, memory, tokenSpeed } = ctx;
10
10
  const left = [];
11
11
  const right = [];
12
12
  // Model
@@ -27,7 +27,19 @@ export function renderLine1(ctx, c) {
27
27
  }
28
28
  left.push(branchStr);
29
29
  }
30
- // Directory
30
+ // Repo segment — owner/name from workspace.repo, clickable to open the repo
31
+ // on its host. Sits right after branch: both are git-identity (where you are
32
+ // in the DAG + which remote it tracks), so they stay grouped. Distinct from
33
+ // the directory breadcrumb below (a local path): this is the canonical remote
34
+ // identity, and surfaces the owner that the bare cwd basename can't.
35
+ if (display.repo && input.repo) {
36
+ const { owner, name, url } = input.repo;
37
+ const repoLen = cols < 80 ? 14 : cols < 120 ? 24 : 36;
38
+ const label = c.brightBlue(`${icons.repo} ${truncField(`${owner}/${name}`, repoLen)}`);
39
+ left.push(hyperlink(url, label));
40
+ }
41
+ // Directory — local filesystem context (where on disk), orthogonal to the
42
+ // git identity above. Rendered after repo so the two git segments group.
31
43
  if (display.directory) {
32
44
  const cwd = input.cwd;
33
45
  if (cwd) {
@@ -40,16 +52,6 @@ export function renderLine1(ctx, c) {
40
52
  left.push(hyperlink(pathToFileURL(cwd).href, label));
41
53
  }
42
54
  }
43
- // Repo segment — owner/name from workspace.repo, clickable to open the repo
44
- // on its host. Distinct from the directory breadcrumb above (a local path):
45
- // this is the canonical remote identity, and surfaces the owner that the
46
- // bare cwd basename can't.
47
- if (display.repo && input.repo) {
48
- const { owner, name, url } = input.repo;
49
- const repoLen = cols < 80 ? 14 : cols < 120 ? 24 : 36;
50
- const label = c.brightBlue(`${icons.repo} ${truncField(`${owner}/${name}`, repoLen)}`);
51
- left.push(hyperlink(url, label));
52
- }
53
55
  // Added dirs badge — only when count > 0; warning color at >= 5
54
56
  if (display.addedDirs && input.addedDirsCount != null && input.addedDirsCount > 0) {
55
57
  const badge = `+${input.addedDirsCount} dirs`;
@@ -118,8 +120,11 @@ export function renderLine1(ctx, c) {
118
120
  right.push(c.gray(input.outputStyle));
119
121
  }
120
122
  // Version — link to the Claude Code npm page for quick changelog lookup.
123
+ // Kept as its own segment (not pushed straight into `right`) because packed
124
+ // mode pins it alone to the true right edge while everything else packs left.
125
+ let versionSeg = null;
121
126
  if (display.version && input.version) {
122
- right.push(hyperlink(`https://www.npmjs.com/package/@anthropic-ai/claude-code/v/${encodeURIComponent(input.version)}`, c.dim(`v${input.version}`)));
127
+ versionSeg = hyperlink(`https://www.npmjs.com/package/@anthropic-ai/claude-code/v/${encodeURIComponent(input.version)}`, c.dim(`v${input.version}`));
123
128
  }
124
129
  // Custom commands (issue #143 phase 3) — appended last on the left so they
125
130
  // sit after core widgets and evict first under fitSegments' narrow-cols
@@ -129,8 +134,18 @@ export function renderLine1(ctx, c) {
129
134
  if (seg)
130
135
  left.push(seg);
131
136
  }
132
- if (left.length === 0 && right.length === 0)
137
+ if (left.length === 0 && right.length === 0 && !versionSeg)
133
138
  return '';
134
- return fitSegments(left, right, SEP, cols);
139
+ // packed: pack every non-version segment tightly to the left with a single
140
+ // separator (no forced middle gap), leaving ONLY the version string pinned to
141
+ // the true right edge — the same idiom line2 uses for its small right-anchored
142
+ // cluster. fitSegments already leaves a gap before a short right array, and
143
+ // drops it whole when it can't fit (version vanishes rather than truncating on
144
+ // a narrow terminal — an accepted all-or-nothing tradeoff, see README).
145
+ if (line1Align === 'packed') {
146
+ return fitSegments(left.concat(right), versionSeg ? [versionSeg] : [], SEP, cols);
147
+ }
148
+ // justified: version rejoins the tail of the right cluster, pinned to the edge.
149
+ return fitSegments(left, versionSeg ? [...right, versionSeg] : right, SEP, cols);
135
150
  }
136
151
  //# sourceMappingURL=line1.js.map
@@ -69,27 +69,28 @@ function buildSegments(ctx, palette, c) {
69
69
  priority: 80,
70
70
  });
71
71
  }
72
- if (display.directory && input.cwd) {
73
- const dirName = basename(input.cwd) || input.cwd;
72
+ if (display.repo && input.repo) {
73
+ const { owner, name, url } = input.repo;
74
+ // Priority 61 — above directory@60. Repo is git identity (owner/name of the
75
+ // remote), grouped with branch, so it survives narrow-terminal pressure a
76
+ // step longer than the local directory. Rendered before directory to keep
77
+ // the git-identity segments adjacent. Same dir-family background.
74
78
  segments.push({
75
- text: hyperlink(pathToFileURL(input.cwd).href, truncField(dirName, 30)),
76
- icon: icons.folder,
79
+ text: hyperlink(url, truncField(`${owner}/${name}`, 36)),
80
+ icon: icons.repo,
77
81
  bg: palette.dirBg,
78
82
  fg: palette.fg,
79
- priority: 60,
83
+ priority: 61,
80
84
  });
81
85
  }
82
- if (display.repo && input.repo) {
83
- const { owner, name, url } = input.repo;
84
- // Priority 58 — below directory@60 and the addedDirs badge@59. Repo is the
85
- // canonical remote identity (annotates the local dir), so it cedes first
86
- // under narrow-terminal pressure. Same dir-family background as directory.
86
+ if (display.directory && input.cwd) {
87
+ const dirName = basename(input.cwd) || input.cwd;
87
88
  segments.push({
88
- text: hyperlink(url, truncField(`${owner}/${name}`, 36)),
89
- icon: icons.repo,
89
+ text: hyperlink(pathToFileURL(input.cwd).href, truncField(dirName, 30)),
90
+ icon: icons.folder,
90
91
  bg: palette.dirBg,
91
92
  fg: palette.fg,
92
- priority: 58,
93
+ priority: 60,
93
94
  });
94
95
  }
95
96
  if (display.addedDirs && input.addedDirsCount != null && input.addedDirsCount > 0) {
@@ -15,7 +15,11 @@ export function displayWidth(str) {
15
15
  w += 1;
16
16
  continue;
17
17
  }
18
- if (cp >= 0x1F000 || (cp >= 0x2300 && cp <= 0x257F) || (cp >= 0x25A0 && cp <= 0x25FF) ||
18
+ // 0x2300–0x24FF covers wide Misc-Technical / Enclosed-Alphanumeric symbols
19
+ // but STOPS before the Box Drawing block (U+2500–U+257F), whose glyphs —
20
+ // including │ (U+2502), lumira's default SEP separator — are single-cell.
21
+ // Counting them as 2 over-reserved width on every separated line.
22
+ if (cp >= 0x1F000 || (cp >= 0x2300 && cp <= 0x24FF) || (cp >= 0x25A0 && cp <= 0x25FF) ||
19
23
  (cp >= 0x2600 && cp <= 0x27BF) || (cp >= 0x2B00 && cp <= 0x2BFF) ||
20
24
  (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3000 && cp <= 0x303F) || (cp >= 0xFF00 && cp <= 0xFFEF)) {
21
25
  w += 2;
@@ -53,13 +57,21 @@ export function truncatePath(str, maxLen = 20) {
53
57
  return filename.slice(0, maxLen - 3) + '...';
54
58
  return '.../' + filename;
55
59
  }
60
+ // The usable width for a status line: total cols minus a 4-cell right margin.
61
+ // The margin absorbs terminals that reserve a trailing cell and prevents the
62
+ // off-by-one wraps that motivated it. Kept as the single source of this
63
+ // constant so callers computing leftover/fill width can't drift out of sync
64
+ // with fitSegments' own bound.
65
+ export function safeCols(cols) {
66
+ return Math.max(1, cols - 4);
67
+ }
56
68
  export function fitSegments(left, right, sep, cols) {
57
- const safeCols = Math.max(1, cols - 4);
69
+ const safe = safeCols(cols);
58
70
  for (let l = left.length; l >= 1; l--) {
59
71
  const lSlice = left.slice(0, l);
60
72
  const leftStr = lSlice.join(sep);
61
73
  const leftW = displayWidth(leftStr);
62
- if (leftW > safeCols)
74
+ if (leftW > safe)
63
75
  continue;
64
76
  for (let r = right.length; r >= 0; r--) {
65
77
  const rSlice = right.slice(0, r);
@@ -67,8 +79,8 @@ export function fitSegments(left, right, sep, cols) {
67
79
  return leftStr;
68
80
  const rightStr = rSlice.join(sep);
69
81
  const rightW = displayWidth(rightStr);
70
- if (leftW + 1 + rightW <= safeCols) {
71
- const gap = Math.max(1, safeCols - leftW - rightW);
82
+ if (leftW + 1 + rightW <= safe) {
83
+ const gap = Math.max(1, safe - leftW - rightW);
72
84
  return leftStr + ' '.repeat(gap) + rightStr;
73
85
  }
74
86
  }
@@ -77,7 +89,7 @@ export function fitSegments(left, right, sep, cols) {
77
89
  // Safe because left[0] is the model name (~20 chars) — callers must ensure
78
90
  // the first segment is short enough to truncate gracefully.
79
91
  // Strip ANSI before hard-truncating to avoid cutting mid-escape-sequence.
80
- return truncField(stripAnsi(left[0] ?? ''), safeCols);
92
+ return truncField(stripAnsi(left[0] ?? ''), safe);
81
93
  }
82
94
  export function padLine(left, right, cols) {
83
95
  const leftW = displayWidth(left);
package/dist/types.js CHANGED
@@ -123,6 +123,7 @@ export const DEFAULT_DISPLAY = {
123
123
  };
124
124
  export const DEFAULT_CONFIG = {
125
125
  layout: 'auto',
126
+ line1Align: 'justified',
126
127
  // GSD on by default, mirroring GSD's own always-on statusline. Self-gates to
127
128
  // nothing when there's no .planning/STATE.md and no update-check cache, so
128
129
  // non-GSD users see no extra line and pay only a few cheap existsSync checks.
@@ -46,12 +46,17 @@ export function getTermCols() {
46
46
  return 120;
47
47
  }
48
48
  // When stdout isn't a TTY (the statusline case — Claude Code pipes our output)
49
- // we still trust the resolved rawCols since proc-tree / COLUMNS / tput give the
50
- // real terminal width. The small 0.9 factor leaves 10% headroom for any chrome
51
- // the host renderer adds (separators, gutters) without aggressively starving
52
- // segments. Was 0.7 historically too conservative for CC, where the
53
- // statusline uses the full terminal width.
54
- export function getLayoutCols(rawCols, isTTY, factor = 0.9) {
49
+ // we trust the resolved rawCols since proc-tree / COLUMNS / tput give the real
50
+ // terminal width, and use the full width (factor 1.0). Earlier versions reserved
51
+ // headroom (0.7, then 0.9) on the theory that CC appends chrome (separators,
52
+ // gutters) to the statusline's own row. Inspecting a real render showed that is
53
+ // not the case: CC's own hints ("bypass permissions …", "PR #…", "← for agents")
54
+ // render on a SEPARATE line below the statusline, never appended to its row — so
55
+ // the reservation was speculative and only ever showed up as empty space at the
56
+ // far-right edge. Every renderer that treats `cols` as a width budget already
57
+ // protects itself: line1/line2 via fitSegments and the powerline lines via
58
+ // renderPowerline both subtract a proven 4-column margin (see text.ts / powerline.ts).
59
+ export function getLayoutCols(rawCols, isTTY, factor = 1.0) {
55
60
  if (isTTY)
56
61
  return rawCols;
57
62
  const clamped = Math.min(Math.max(factor, 0.3), 1.0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "description": "Real-time statusline HUD for Claude Code and Qwen Code. Includes session analytics CLI, API latency overhead widget, 7d quota projection, auto-compact proximity warnings, themes, and powerline. Zero deps.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,7 @@ Your job: read the user's current config, translate their natural-language reque
30
30
  | `icons` | string | `"nerd"` \| `"emoji"` \| `"none"` | Default `"nerd"` (requires Nerd Font) |
31
31
  | `theme` | string | `"dracula"` \| `"nord"` \| `"tokyo-night"` \| `"catppuccin"` \| `"monokai"` \| `"gruvbox"` \| `"solarized"` | Requires `colors.mode: "truecolor"` to take effect |
32
32
  | `style` | string | `"classic"` \| `"powerline"` | Visual style for line 1 |
33
+ | `line1Align` | string | `"justified"` \| `"packed"` | Line 1 classic layout. `"justified"` (default) pins the left cluster to the start and the right cluster to the end. `"packed"` packs everything tightly to the left, leaving only the version pinned to the true right edge. No-op in powerline mode (always packs). |
33
34
  | `powerline.style` | string | `"arrow"` \| `"flame"` \| `"slant"` \| `"round"` \| `"diamond"` \| `"compatible"` \| `"plain"` \| `"auto"` | Separator preset; only meaningful when `style: "powerline"` |
34
35
  | `colors.mode` | string | `"auto"` \| `"named"` \| `"256"` \| `"truecolor"` | Color depth |
35
36
  | `gsd` | boolean | `true` \| `false` | Show GSD task info section |
@@ -81,6 +82,13 @@ User intent → minimal JSON patch (assume existing config is preserved unless e
81
82
  }
82
83
  ```
83
84
 
85
+ **"pegá todo a la izquierda, sacá el hueco del medio"** / "packed line 1"
86
+ ```json
87
+ {
88
+ "line1Align": "packed"
89
+ }
90
+ ```
91
+
84
92
  **"hide cost and tokens, keep everything else"** (patch only the toggles requested)
85
93
  ```json
86
94
  {