lumira 1.10.0 → 1.12.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.
@@ -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.10.0"
8
+ "version": "1.12.0"
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.10.0",
15
+ "version": "1.12.0",
16
16
  "author": {
17
17
  "name": "Carlos Cativo"
18
18
  },
@@ -32,5 +32,5 @@
32
32
  ]
33
33
  }
34
34
  ],
35
- "version": "1.10.0"
35
+ "version": "1.12.0"
36
36
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
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/dist/config.js CHANGED
@@ -248,6 +248,8 @@ const PRESET_DEFS = {
248
248
  layout: 'auto',
249
249
  display: {
250
250
  agents: true,
251
+ pr: true,
252
+ thinking: true,
251
253
  burnRate: false,
252
254
  duration: false,
253
255
  tokenSpeed: false,
@@ -294,6 +296,8 @@ const PRESET_DEFS = {
294
296
  addedDirs: false,
295
297
  worktreeBreadcrumb: false,
296
298
  compactionCount: false,
299
+ pr: false,
300
+ thinking: false,
297
301
  },
298
302
  },
299
303
  };
package/dist/normalize.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Single internal format that all renderers can consume.
4
4
  // Platform-specific quirks are handled once here.
5
5
  // Renderers check field presence, not platform identity.
6
- import { AUTO_COMPACT_THRESHOLD, AUTO_COMPACT_WARNING_GAP } from './types.js';
6
+ import { AUTO_COMPACT_THRESHOLD, AUTO_COMPACT_WARNING_GAP, PR_REVIEW_STATES } from './types.js';
7
7
  export function isQwenInput(input) {
8
8
  const raw = input;
9
9
  if (!raw.metrics || typeof raw.metrics !== 'object' || !('models' in raw.metrics))
@@ -28,6 +28,8 @@ export function sanitizeTermString(s) {
28
28
  }
29
29
  /** Allowed values for the reasoning effort level field (CC ≥ 2.1.x). */
30
30
  const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
31
+ /** Allowed values for PR review state (CC ≥ 2.1.145). */
32
+ const VALID_PR_REVIEW_STATES = new Set(PR_REVIEW_STATES);
31
33
  /**
32
34
  * Sum input token categories from `context_window.current_usage` to compute
33
35
  * a real context usage total (input + cache_read + cache_creation).
@@ -174,6 +176,30 @@ export function normalize(input) {
174
176
  const cacheHitRate = (cached != null && cacheTurnDenominator && platform === 'claude-code')
175
177
  ? Math.min(100, Math.round((cached / cacheTurnDenominator) * 100))
176
178
  : undefined;
179
+ // PR widget (Claude only, CC ≥ 2.1.145).
180
+ // number must be a positive integer — drop the whole object if invalid.
181
+ // url: sanitize then accept only https:// scheme (OSC 8 injection guard).
182
+ // reviewState: sanitize then gate against the PR_REVIEW_STATES allowlist.
183
+ let pr;
184
+ if (claude?.pr != null) {
185
+ const rawPr = claude.pr;
186
+ const n = rawPr.number;
187
+ if (typeof n === 'number' && Number.isInteger(n) && n > 0) {
188
+ let prUrl;
189
+ if (typeof rawPr.url === 'string') {
190
+ const sanitized = sanitizeTermString(rawPr.url);
191
+ if (sanitized.startsWith('https://'))
192
+ prUrl = sanitized;
193
+ }
194
+ let prReviewState;
195
+ if (typeof rawPr.review_state === 'string') {
196
+ const sanitized = sanitizeTermString(rawPr.review_state);
197
+ if (VALID_PR_REVIEW_STATES.has(sanitized))
198
+ prReviewState = sanitized;
199
+ }
200
+ pr = { number: n, url: prUrl, reviewState: prReviewState };
201
+ }
202
+ }
177
203
  return {
178
204
  platform,
179
205
  model: sanitizeTermString(modelName),
@@ -208,6 +234,7 @@ export function normalize(input) {
208
234
  effortLevel: claude?.effort?.level && VALID_EFFORT_LEVELS.has(claude.effort.level)
209
235
  ? sanitizeTermString(claude.effort.level)
210
236
  : undefined,
237
+ thinkingEnabled: claude?.thinking?.enabled === true ? true : undefined,
211
238
  worktreeName: input.worktree?.name ? sanitizeTermString(input.worktree.name) : undefined,
212
239
  addedDirsCount: (() => {
213
240
  const dirs = input.workspace?.added_dirs;
@@ -223,6 +250,7 @@ export function normalize(input) {
223
250
  })(),
224
251
  rateLimits,
225
252
  cacheHitRate,
253
+ pr,
226
254
  raw: input,
227
255
  };
228
256
  }
@@ -60,6 +60,8 @@ export const NERD_ICONS = {
60
60
  car: '🏎️', // racing car — pace-ahead indicator
61
61
  turtle: '🐢', // turtle — pace-behind indicator
62
62
  lightning: '⚡', // lightning bolt — cache hit rate
63
+ pr: '', // nf-cod-git_pull_request
64
+ thinking: '󱠤', // nf-md-brain
63
65
  battery: nerdBattery,
64
66
  };
65
67
  export const EMOJI_ICONS = {
@@ -83,6 +85,8 @@ export const EMOJI_ICONS = {
83
85
  car: '\u{1F3CE}️', // 🏎️ — racing car
84
86
  turtle: '\u{1F422}', // 🐢 — turtle
85
87
  lightning: '⚡', // ⚡ — cache hit rate
88
+ pr: '\u{1F500}', // 🔀 — twisted rightwards arrows (PR)
89
+ thinking: '\u{1F4AD}', // 💭 — thought bubble
86
90
  battery: (pct) => {
87
91
  if (!Number.isFinite(pct) || pct < 0)
88
92
  return '\u{1F50B}'; // 🔋 — no data / invalid input
@@ -114,6 +118,8 @@ export const NO_ICONS = {
114
118
  car: '',
115
119
  turtle: '',
116
120
  lightning: '',
121
+ pr: 'PR',
122
+ thinking: 'think',
117
123
  // No-icon mode keeps the legacy bolt fallback (currently empty) so users who
118
124
  // opted out of icons see no shape change from this feature.
119
125
  battery: () => '',
@@ -7,6 +7,7 @@ import { formatTokens, formatCost, formatBurnRate } from '../utils/format.js';
7
7
  import { getConfigHealth } from '../parsers/config-health.js';
8
8
  import { computePaceDelta, formatPaceDelta } from './pace.js';
9
9
  import { computeQuotaProjection, formatProjectionWarning } from './quota-projection.js';
10
+ import { hyperlink } from './hyperlink.js';
10
11
  const SEVEN_DAY_WINDOW_SEC = 7 * 24 * 3600;
11
12
  const SEVEN_DAY_MIN_ELAPSED_SEC = 3600; // 1h floor — see quota-projection.ts
12
13
  export function formatCountdown(resetsAt) {
@@ -115,6 +116,18 @@ export function renderLine2(ctx, c) {
115
116
  leftParts.push(c.dim(`MCP ${total}`));
116
117
  }
117
118
  }
119
+ // PR widget (CC ≥ 2.1.145)
120
+ if (display.pr && input.pr) {
121
+ const { number, url, reviewState } = input.pr;
122
+ const stateStr = reviewState ?? '';
123
+ const colorFn = reviewState === 'approved' ? c.green :
124
+ reviewState === 'pending' ? c.yellow :
125
+ reviewState === 'changes_requested' ? c.orange :
126
+ c.dim; // draft or unknown
127
+ const text = `${icons.pr} #${number}${stateStr ? ` ${stateStr}` : ''}`;
128
+ const colored = colorFn(text);
129
+ leftParts.push(url ? hyperlink(url, colored) : colored);
130
+ }
118
131
  // Qwen metrics (shared helper)
119
132
  leftParts.push(...formatQwenMetrics(input, c, icons));
120
133
  // 7d quota projection — computed once, surfaced two ways depending on whether
@@ -233,6 +246,9 @@ export function renderLine2(ctx, c) {
233
246
  if (display.effort && effort && effort !== 'medium') {
234
247
  rightParts.push(c.dim(`^${effort}`));
235
248
  }
249
+ if (display.thinking && input.thinkingEnabled) {
250
+ rightParts.push(c.dim(icons.thinking));
251
+ }
236
252
  // Config health hints (opt-in, default off). Sit on the right side as
237
253
  // auxiliary signals next to vim/effort, and are dropped silently when the
238
254
  // projected line width would overflow `cols` — they are advisory, never
@@ -21,6 +21,20 @@ function getCacheHitBg(rate, palette) {
21
21
  case 'critical': return palette.branchDirtyBg;
22
22
  }
23
23
  }
24
+ // Maps PR review state to a powerline bg slot (urgency via background, not text color).
25
+ // approved → dirBg (neutral — PR is ready, low urgency)
26
+ // pending → taskBg (warm — waiting on reviewers)
27
+ // changes_requested → branchDirtyBg (hot — action needed from author)
28
+ // draft → dirBg (neutral — not ready for review)
29
+ function getPrReviewBg(reviewState, palette) {
30
+ switch (reviewState) {
31
+ case 'changes_requested': return palette.branchDirtyBg;
32
+ case 'pending': return palette.taskBg;
33
+ case 'approved':
34
+ case 'draft':
35
+ default: return palette.dirBg;
36
+ }
37
+ }
24
38
  // Maps the API latency tier (SSOT in colors.ts) to a powerline bg slot.
25
39
  // healthy/notable → dirBg (neutral; api is fast or only slightly slow)
26
40
  // warn → taskBg (warm; api is dominating session time)
@@ -210,6 +224,14 @@ function buildSegments(ctx, palette, c) {
210
224
  const mcpText = errors > 0 ? `MCP ${total - errors}/${total}` : `MCP ${total}`;
211
225
  segments.push({ text: mcpText, bg: palette.taskBg, fg: palette.fg, priority: 50 });
212
226
  }
227
+ // PR widget (CC ≥ 2.1.145)
228
+ if (display.pr && input.pr) {
229
+ const { number, reviewState } = input.pr;
230
+ const stateStr = reviewState ?? '';
231
+ const text = `${icons.pr} #${number}${stateStr ? ` ${stateStr}` : ''}`;
232
+ const bg = getPrReviewBg(reviewState, palette);
233
+ segments.push({ text, bg, fg: palette.fg, priority: 55 });
234
+ }
213
235
  // Qwen metrics
214
236
  const qwenParts = formatQwenMetrics(input, c, icons);
215
237
  for (const part of qwenParts) {
@@ -224,6 +246,11 @@ function buildSegments(ctx, palette, c) {
224
246
  if (display.effort && effort && effort !== 'medium') {
225
247
  segments.push({ text: `^${effort}`, bg: palette.versionBg, fg: palette.fg, priority: 30 });
226
248
  }
249
+ // Extended thinking indicator — priority 28 (one below effort/vim at 30) so it
250
+ // evicts before them on narrow terminals; thinking is informational, not urgent.
251
+ if (display.thinking && input.thinkingEnabled) {
252
+ segments.push({ text: icons.thinking, bg: palette.dirBg, fg: palette.fg, priority: 28 });
253
+ }
227
254
  // Config health hints (opt-in, lowest priority — evicted first on narrow terminals)
228
255
  if (display.health && input.cwd) {
229
256
  const colorMode = ctx.config.colors.mode === 'auto' ? detectColorMode() : ctx.config.colors.mode;
package/dist/types.js CHANGED
@@ -38,6 +38,7 @@ export const CUSTOM_COMMAND_COLORS = ['dim', 'green', 'yellow', 'orange', 'red',
38
38
  * `POWERLINE_STYLES` in `src/render/powerline.ts` — that map's keys
39
39
  * MUST match this list.
40
40
  */
41
+ export const PR_REVIEW_STATES = ['approved', 'pending', 'changes_requested', 'draft'];
41
42
  export const POWERLINE_STYLE_NAMES = [
42
43
  'arrow', 'flame', 'slant', 'round', 'diamond', 'compatible', 'plain', 'auto',
43
44
  ];
@@ -108,6 +109,8 @@ export const DEFAULT_DISPLAY = {
108
109
  addedDirs: true,
109
110
  worktreeBreadcrumb: true,
110
111
  compactionCount: true,
112
+ pr: true,
113
+ thinking: true,
111
114
  contextWarningThreshold: DEFAULT_CONTEXT_WARNING_THRESHOLD,
112
115
  contextCriticalThreshold: DEFAULT_CONTEXT_CRITICAL_THRESHOLD,
113
116
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.10.0",
3
+ "version": "1.12.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",