lumira 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
26
26
 
27
27
  > 3,400+ monthly downloads, zero marketing. Try it for one session — `npx lumira install`.
28
28
 
29
- > **What's new in v1.5:** the [`lumira stats` CLI](#stats-cli) prints post-session analytics (cost, tokens, cache hit, tool frequency, burn rate). Combined with the `API N%` latency widget (v1.4.0), 7-day quota projection warning (v1.3.0), and auto-compact proximity glyph ⚠ (v1.4.1), recent releases add several diagnostic signals to the session.
29
+ > **What's new in v1.7.0:** added-dirs badge (`+N dirs`, orange at ≥5) and worktree origin-branch breadcrumb (`↳ <branch>`) both togglable via `display.addedDirs` / `display.worktreeBreadcrumb`, on by default in `full`/`balanced`. Combined with the [`lumira stats` CLI](#stats-cli) (v1.5), `API N%` latency widget (v1.4.0), 7-day quota projection warning (v1.3.0), and auto-compact proximity glyph ⚠ (v1.4.1), recent releases add several diagnostic signals to the session.
30
30
 
31
31
  ## Table of contents
32
32
 
@@ -87,7 +87,7 @@ Inspired by [claude-hud](https://github.com/jarrodwatts/claude-hud); takes a dif
87
87
  - **Config health widget** — surfaces silent fallbacks (theme/powerline degrading in named-ANSI, missing GSD STATE.md). Opt-in.
88
88
  - **Memory usage** — process RSS percentage.
89
89
  - **MCP server detection** — count of attached MCP servers per session.
90
- - **Vim-mode hint, thinking effort, worktree, output style, session name** — all togglable per-field via `display.*`.
90
+ - **Vim-mode hint, thinking effort, worktree, output style, session name, added-dirs badge, worktree origin-branch breadcrumb** — all togglable per-field via `display.*`.
91
91
  - **3-tier color system** — named ANSI / 256-color / truecolor, auto-detected.
92
92
  - **Config-driven** — every feature toggleable via JSON config + CLI flags.
93
93
 
@@ -269,6 +269,7 @@ Create `~/.config/lumira/config.json`:
269
269
  "burnRate": true,
270
270
  "duration": true,
271
271
  "tokenSpeed": true,
272
+ "apiLatency": true,
272
273
  "rateLimits": true,
273
274
  "paceDelta": true,
274
275
  "quotaProjection": true,
@@ -278,6 +279,8 @@ Create `~/.config/lumira/config.json`:
278
279
  "vim": true,
279
280
  "effort": true,
280
281
  "worktree": true,
282
+ "addedDirs": true,
283
+ "worktreeBreadcrumb": true,
281
284
  "agent": true,
282
285
  "sessionName": true,
283
286
  "style": true,
package/dist/config.js CHANGED
@@ -291,6 +291,8 @@ const PRESET_DEFS = {
291
291
  // surface (see burnRate/rateLimits/paceDelta etc. above). Default
292
292
  // remains true; users on full/balanced see the widget out of the box.
293
293
  apiLatency: false,
294
+ addedDirs: false,
295
+ worktreeBreadcrumb: false,
294
296
  },
295
297
  },
296
298
  };
package/dist/normalize.js CHANGED
@@ -191,6 +191,18 @@ export function normalize(input) {
191
191
  ? sanitizeTermString(claude.effort.level)
192
192
  : undefined,
193
193
  worktreeName: input.worktree?.name ? sanitizeTermString(input.worktree.name) : undefined,
194
+ addedDirsCount: (() => {
195
+ const dirs = input.workspace?.added_dirs;
196
+ if (!Array.isArray(dirs) || dirs.length === 0)
197
+ return undefined;
198
+ return dirs.length;
199
+ })(),
200
+ worktreeOriginalBranch: (() => {
201
+ const orig = input.worktree?.original_branch;
202
+ if (!orig || typeof orig !== 'string')
203
+ return undefined;
204
+ return sanitizeTermString(orig);
205
+ })(),
194
206
  rateLimits,
195
207
  cacheHitRate,
196
208
  raw: input,
@@ -0,0 +1,10 @@
1
+ export function computeBurnExtrapolation(usedPct, elapsedSec, remainingSec) {
2
+ const windowSec = elapsedSec + remainingSec;
3
+ const elapsedPct = (elapsedSec / windowSec) * 100;
4
+ const delta = usedPct - elapsedPct;
5
+ const burnRateSec = usedPct / elapsedSec;
6
+ const timeToExhaustSec = (100 - usedPct) / burnRateSec;
7
+ const willExhaustBefore = timeToExhaustSec < remainingSec;
8
+ return { burnRateSec, elapsedSec, remainingSec, delta, timeToExhaustSec, willExhaustBefore };
9
+ }
10
+ //# sourceMappingURL=burn-math.js.map
@@ -40,6 +40,18 @@ export function renderLine1(ctx, c) {
40
40
  left.push(hyperlink(pathToFileURL(cwd).href, label));
41
41
  }
42
42
  }
43
+ // Added dirs badge — only when count > 0; warning color at >= 5
44
+ if (display.addedDirs && input.addedDirsCount != null && input.addedDirsCount > 0) {
45
+ const badge = `+${input.addedDirsCount} dirs`;
46
+ left.push(input.addedDirsCount >= 5 ? c.orange(badge) : c.dim(badge));
47
+ }
48
+ // Worktree origin-branch breadcrumb — only when original_branch is present,
49
+ // there IS a current branch to contrast against (anchor), and they differ.
50
+ const branchForBreadcrumb = input.gitBranch || git.branch;
51
+ if (display.worktreeBreadcrumb && input.worktreeOriginalBranch && branchForBreadcrumb && input.worktreeOriginalBranch !== branchForBreadcrumb) {
52
+ const truncated = truncField(input.worktreeOriginalBranch, 15);
53
+ left.push(c.gray(`↳ ${truncated}`));
54
+ }
43
55
  // Duration (Claude only)
44
56
  if (display.duration && input.durationMs != null) {
45
57
  right.push(c.dim(`${icons.clock} ${formatDuration(input.durationMs)}`));
@@ -1,4 +1,5 @@
1
1
  import { debug } from '../utils/debug.js';
2
+ import { computeBurnExtrapolation } from './burn-math.js';
2
3
  const log = debug('pace');
3
4
  export function computePaceDelta(usedPercentage, resetsAt, nowSec) {
4
5
  const now = nowSec ?? Date.now() / 1000;
@@ -15,15 +16,12 @@ export function computePaceDelta(usedPercentage, resetsAt, nowSec) {
15
16
  log({ reason: 'insufficient data (<5min)', elapsedSec, remainingSec });
16
17
  return null;
17
18
  }
18
- const elapsedPct = (elapsedSec / totalWindowSec) * 100;
19
- const delta = usedPercentage - elapsedPct;
20
- let timeToExhaustion = null;
21
- if (delta > 0 && usedPercentage > 0) {
22
- // burn rate = usedPercentage / elapsedSec (pct per second)
23
- // time to exhaust remaining (100 - usedPercentage) pct at that rate
24
- timeToExhaustion = (100 - usedPercentage) / (usedPercentage / elapsedSec) / 60;
25
- }
19
+ const burn = computeBurnExtrapolation(usedPercentage, elapsedSec, remainingSec);
20
+ const timeToExhaustion = burn.delta > 0 && usedPercentage > 0
21
+ ? burn.timeToExhaustSec / 60
22
+ : null;
26
23
  if (log.enabled) {
24
+ const elapsedPct = (elapsedSec / totalWindowSec) * 100;
27
25
  log({
28
26
  usedPercentage,
29
27
  resetsAt,
@@ -33,11 +31,11 @@ export function computePaceDelta(usedPercentage, resetsAt, nowSec) {
33
31
  remainingSec: Math.round(remainingSec),
34
32
  remainingMin: Math.round(remainingSec / 60),
35
33
  elapsedPct: Math.round(elapsedPct * 10) / 10,
36
- delta: Math.round(delta * 10) / 10,
34
+ delta: Math.round(burn.delta * 10) / 10,
37
35
  timeToExhaustionMin: timeToExhaustion != null ? Math.round(timeToExhaustion) : null,
38
36
  });
39
37
  }
40
- return { delta, timeToExhaustion };
38
+ return { delta: burn.delta, timeToExhaustion };
41
39
  }
42
40
  export function formatPaceDelta(pace) {
43
41
  const rounded = Math.round(pace.delta);
@@ -11,19 +11,21 @@ import { derivePowerlinePalette, DEFAULT_POWERLINE_PALETTE, } from '../themes.js
11
11
  /**
12
12
  * Build the line1 segment list for the powerline renderer. Segment priorities
13
13
  * control which get dropped first when the terminal is narrow (lower = drops first):
14
- * model: 100 (always kept)
15
- * branch: 80
16
- * dir: 60
17
- * task: 40
18
- * duration: 35
19
- * memory: 30
20
- * tokenSpeed: 25
21
- * linesChanged: 24
22
- * worktree: 23
23
- * agent: 22
24
- * sessionName: 21
25
- * version: 20
26
- * style: 18 (dropped first)
14
+ * model: 100 (always kept)
15
+ * branch: 80
16
+ * dir: 60
17
+ * addedDirs: 59 (renders after dir; data-gated; evicts before dir under pressure)
18
+ * task: 40
19
+ * duration: 35
20
+ * memory: 30
21
+ * tokenSpeed: 25
22
+ * linesChanged: 24
23
+ * worktree: 23
24
+ * worktreeBreadcrumb: 22.5 (between worktree@23 and agent@22; data-gated)
25
+ * agent: 22
26
+ * sessionName: 21
27
+ * version: 20
28
+ * style: 18 (dropped first)
27
29
  */
28
30
  function buildSegments(ctx, palette, c) {
29
31
  const { input, git, transcript, config: { display }, icons, memory, tokenSpeed } = ctx;
@@ -77,6 +79,15 @@ function buildSegments(ctx, palette, c) {
77
79
  priority: 60,
78
80
  });
79
81
  }
82
+ if (display.addedDirs && input.addedDirsCount != null && input.addedDirsCount > 0) {
83
+ const badge = `+${input.addedDirsCount} dirs`;
84
+ const bg = input.addedDirsCount >= 5 ? palette.taskBg : palette.versionBg;
85
+ // Priority 59 — one BELOW directory@60. The badge annotates the directory,
86
+ // so it must evict before the directory under narrow-terminal pressure
87
+ // (applyPriorityEviction drops lowest-priority first). Insertion order is
88
+ // unchanged, so it still renders right after the directory.
89
+ segments.push({ text: badge, bg, fg: palette.fg, priority: 59 });
90
+ }
80
91
  const activeTask = getActiveTodo(transcript);
81
92
  if (activeTask) {
82
93
  segments.push({
@@ -136,6 +147,19 @@ function buildSegments(ctx, palette, c) {
136
147
  priority: 23,
137
148
  });
138
149
  }
150
+ if (display.worktreeBreadcrumb && input.worktreeOriginalBranch) {
151
+ const currentBranch = input.gitBranch || git.branch;
152
+ // Only render when there is a current branch to contrast against — the
153
+ // breadcrumb is meaningless without an anchor (no branch shown / branch off).
154
+ if (currentBranch && input.worktreeOriginalBranch !== currentBranch) {
155
+ segments.push({
156
+ text: `↳ ${truncField(input.worktreeOriginalBranch, 15)}`,
157
+ bg: palette.versionBg,
158
+ fg: palette.fg,
159
+ priority: 22.5,
160
+ });
161
+ }
162
+ }
139
163
  // Mirrors classic line1: prefer the explicit input.agentName (subagent
140
164
  // session render), else show the cubes badge when exactly one *named*
141
165
  // subagent is running on the parent. Anonymous agents stay collapsed
@@ -1,4 +1,5 @@
1
1
  import { debug } from '../utils/debug.js';
2
+ import { computeBurnExtrapolation } from './burn-math.js';
2
3
  const log = debug('quota-projection');
3
4
  /**
4
5
  * Extrapolates current burn rate to when the quota would hit 100%.
@@ -27,21 +28,18 @@ export function computeQuotaProjection(usedPct, resetsAt, windowSec, nowSec, min
27
28
  log({ reason: 'insufficient elapsed', elapsedSec, minElapsedSec });
28
29
  return null;
29
30
  }
30
- // burnRate is in pct-per-second
31
- const burnRate = usedPct / elapsedSec;
32
- const timeToExhaustSec = (100 - usedPct) / burnRate;
33
- const willExhaustBefore = timeToExhaustSec < remainingSec;
31
+ const burn = computeBurnExtrapolation(usedPct, elapsedSec, remainingSec);
34
32
  if (log.enabled) {
35
33
  log({
36
34
  usedPct,
37
35
  elapsedSec: Math.round(elapsedSec),
38
36
  remainingSec: Math.round(remainingSec),
39
- burnRate,
40
- timeToExhaustSec: Math.round(timeToExhaustSec),
41
- willExhaustBefore,
37
+ burnRate: burn.burnRateSec,
38
+ timeToExhaustSec: Math.round(burn.timeToExhaustSec),
39
+ willExhaustBefore: burn.willExhaustBefore,
42
40
  });
43
41
  }
44
- return { timeToExhaustSec, willExhaustBefore };
42
+ return { timeToExhaustSec: burn.timeToExhaustSec, willExhaustBefore: burn.willExhaustBefore };
45
43
  }
46
44
  /**
47
45
  * Renders a projection as a short warning string (e.g. "⚠ Mon", "🔥 ~12h").
package/dist/types.js CHANGED
@@ -104,6 +104,8 @@ export const DEFAULT_DISPLAY = {
104
104
  agents: true,
105
105
  health: false,
106
106
  apiLatency: true,
107
+ addedDirs: true,
108
+ worktreeBreadcrumb: true,
107
109
  contextWarningThreshold: DEFAULT_CONTEXT_WARNING_THRESHOLD,
108
110
  contextCriticalThreshold: DEFAULT_CONTEXT_CRITICAL_THRESHOLD,
109
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
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",
@@ -44,7 +44,7 @@ Your job: read the user's current config, translate their natural-language reque
44
44
 
45
45
  Valid keys (anything else must be rejected):
46
46
 
47
- `model`, `branch`, `gitChanges`, `directory`, `contextBar`, `contextTokens`, `tokens`, `cost`, `burnRate`, `duration`, `tokenSpeed`, `rateLimits`, `paceDelta`, `quotaProjection`, `tools`, `todos`, `vim`, `effort`, `worktree`, `agent`, `agents`, `sessionName`, `style`, `version`, `linesChanged`, `memory`, `cacheMetrics`, `mcp`, `health`
47
+ `model`, `branch`, `gitChanges`, `directory`, `contextBar`, `contextTokens`, `tokens`, `cost`, `burnRate`, `duration`, `tokenSpeed`, `rateLimits`, `paceDelta`, `quotaProjection`, `tools`, `todos`, `vim`, `effort`, `worktree`, `agent`, `agents`, `sessionName`, `style`, `version`, `linesChanged`, `memory`, `cacheMetrics`, `mcp`, `health`, `apiLatency`, `addedDirs`, `worktreeBreadcrumb`
48
48
 
49
49
  ### Display thresholds (`display.*`, numeric)
50
50