agy-statusblocks 0.1.2 → 0.1.3

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
@@ -47,14 +47,30 @@ On narrow/split-pane terminals, the layout degrades gracefully to stacked single
47
47
 
48
48
  ```json
49
49
  {
50
- "segments": ["context", "model", "usage", "state", "git"]
50
+ "segments": ["context", "model", "usage", "state", "git"],
51
+ "border": "rounded"
51
52
  }
52
53
  ```
53
54
 
54
- Or via environment:
55
+ Border styles supported:
56
+ - `rounded` (default) — `╭─╮` `╰─╯` `│` `─`
57
+ - `square` — `┌─┐` `└─┘` `│` `─`
58
+ - `double` — `╔═╗` `╚═╝` `║` `═`
59
+ - `thick` — `┏━┓` `┗━┛` `┃` `━`
60
+ - `ascii` — `+-+` `+-+` `|` `-` (clean standard 7-bit ASCII)
61
+ - `none` — clean padded columns without frame borders
62
+
63
+ Or configure via environment variables:
55
64
 
56
65
  ```sh
57
66
  AGY_STATUSBLOCKS_SEGMENTS=context,model,usage
67
+ AGY_STATUSBLOCKS_BORDER=double
68
+ ```
69
+
70
+ Or pass via CLI flag:
71
+
72
+ ```sh
73
+ npx agy-statusblocks preview --border double
58
74
  ```
59
75
 
60
76
  ## Preview
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { join, dirname, basename, sep } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { render } from './layout.js';
7
- import { loadConfig } from './config.js';
7
+ import { loadConfig, normalizeBorderStyle } from './config.js';
8
8
  import { color, c } from './colors.js';
9
9
  import { isValidVersion, isNewerVersion } from './version.js';
10
10
  const PACKAGE_VERSION = (() => {
@@ -134,9 +134,26 @@ function runUninstall() {
134
134
  }
135
135
  console.log(color('✓ agy-statusblocks uninstalled and reverted to built-in status line', c.green));
136
136
  }
137
- function runPreview() {
137
+ function parseBorderFlag(argv) {
138
+ for (let i = 0; i < argv.length; i++) {
139
+ const arg = argv[i];
140
+ if (arg === '--border' || arg === '-b') {
141
+ const next = argv[i + 1];
142
+ if (next)
143
+ return normalizeBorderStyle(next);
144
+ }
145
+ else if (arg && arg.startsWith('--border=')) {
146
+ return normalizeBorderStyle(arg.slice('--border='.length));
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+ function runPreview(borderOverride) {
138
152
  process.env['AGY_STATUSBLOCKS_NO_REMOTE'] = '1';
139
153
  const config = loadConfig();
154
+ if (borderOverride) {
155
+ config.border = borderOverride;
156
+ }
140
157
  const now = Date.now() / 1000;
141
158
  const mockData = {
142
159
  product: 'antigravity',
@@ -186,7 +203,8 @@ function runPreview() {
186
203
  level: 'medium',
187
204
  },
188
205
  };
189
- console.log(`\n${color('agy-statusblocks', c.bold, c.gemini)} preview\n`);
206
+ const styleLabel = config.border ?? 'rounded';
207
+ console.log(`\n${color('agy-statusblocks', c.bold, c.gemini)} preview ${color(`[border: ${styleLabel}]`, c.dim)}\n`);
190
208
  for (const width of [120, 80, 50]) {
191
209
  console.log(color('─'.repeat(width), c.gray));
192
210
  console.log(`${width} cols:`);
@@ -199,12 +217,12 @@ function showHelp() {
199
217
  ${color('agy-statusblocks', c.bold, c.gemini)} — block-based status line for Antigravity CLI (agy)
200
218
 
201
219
  ${color('Usage:', c.bold)}
202
- agy-statusblocks init Install into Antigravity CLI settings
203
- agy-statusblocks update Update installed files (--force to downgrade)
204
- agy-statusblocks preview Preview with mock data at various widths
205
- agy-statusblocks uninstall Revert to built-in statusline
206
- agy-statusblocks --version Print the installed package version
207
- agy-statusblocks help Show this help
220
+ agy-statusblocks init Install into Antigravity CLI settings
221
+ agy-statusblocks update Update installed files (--force to downgrade)
222
+ agy-statusblocks preview [--border <style>] Preview with mock data at various widths
223
+ agy-statusblocks uninstall Revert to built-in statusline
224
+ agy-statusblocks --version Print the installed package version
225
+ agy-statusblocks help Show this help
208
226
 
209
227
  ${color('Blocks:', c.bold)}
210
228
  context Context window fill bar, percentage, token counts
@@ -213,13 +231,22 @@ ${color('Blocks:', c.bold)}
213
231
  state Agent state pill and task/subagent/artifact counters
214
232
  git Branch, staged/modified counts, lines added/removed
215
233
 
234
+ ${color('Borders:', c.bold)}
235
+ rounded (default), square, double, thick, ascii, none
236
+
216
237
  ${color('Customize:', c.bold)}
217
- ~/.agy-statusblocks.json: { "segments": ["context", "model", "usage", "state", "git"] }
238
+ ~/.agy-statusblocks.json: { "segments": ["context", "model", "usage", "state", "git"], "border": "rounded" }
218
239
  Env vars: AGY_STATUSBLOCKS_SEGMENTS=context,model,usage
240
+ AGY_STATUSBLOCKS_BORDER=rounded|square|double|thick|ascii|none
241
+ CLI flags: --border, -b <style>
219
242
  `);
220
243
  }
221
- const cmd = process.argv[2];
222
- switch (cmd) {
244
+ const borderFlag = parseBorderFlag(process.argv.slice(2));
245
+ if (borderFlag) {
246
+ process.env['AGY_STATUSBLOCKS_BORDER'] = borderFlag;
247
+ }
248
+ const firstPositional = process.argv.slice(2).find(arg => !arg.startsWith('-'));
249
+ switch (firstPositional) {
223
250
  case 'init':
224
251
  runInit();
225
252
  break;
@@ -227,18 +254,26 @@ switch (cmd) {
227
254
  runUpdate(process.argv.includes('--force'));
228
255
  break;
229
256
  case 'preview':
230
- runPreview();
257
+ runPreview(borderFlag);
231
258
  break;
232
259
  case 'uninstall':
233
260
  runUninstall();
234
261
  break;
235
- case '--version':
236
- case '-v':
237
- console.log(PACKAGE_VERSION);
262
+ case undefined:
263
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
264
+ console.log(PACKAGE_VERSION);
265
+ }
266
+ else if (process.argv.includes('--help') || process.argv.includes('-h')) {
267
+ showHelp();
268
+ }
269
+ else if (process.stdin.isTTY) {
270
+ showHelp();
271
+ }
272
+ else {
273
+ import('./index.js');
274
+ }
238
275
  break;
239
276
  case 'help':
240
- case '--help':
241
- case '-h':
242
277
  showHelp();
243
278
  break;
244
279
  default:
package/dist/config.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- import type { StatusBlocksConfig } from './types.js';
1
+ import type { StatusBlocksConfig, BorderStyle } from './types.js';
2
+ export declare const VALID_BORDER_STYLES: readonly BorderStyle[];
3
+ export declare function normalizeBorderStyle(val: unknown): BorderStyle | undefined;
2
4
  export declare function loadConfig(): StatusBlocksConfig;
package/dist/config.js CHANGED
@@ -1,8 +1,26 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
+ export const VALID_BORDER_STYLES = [
5
+ 'rounded',
6
+ 'square',
7
+ 'double',
8
+ 'thick',
9
+ 'ascii',
10
+ 'none',
11
+ ];
12
+ export function normalizeBorderStyle(val) {
13
+ if (typeof val === 'string') {
14
+ const lower = val.trim().toLowerCase();
15
+ if (VALID_BORDER_STYLES.includes(lower)) {
16
+ return lower;
17
+ }
18
+ }
19
+ return undefined;
20
+ }
4
21
  export function loadConfig() {
5
22
  const envSegments = process.env['AGY_STATUSBLOCKS_SEGMENTS'];
23
+ const envBorder = process.env['AGY_STATUSBLOCKS_BORDER'];
6
24
  const config = {};
7
25
  try {
8
26
  const raw = readFileSync(join(homedir(), '.agy-statusblocks.json'), 'utf8');
@@ -14,10 +32,20 @@ export function loadConfig() {
14
32
  config.remoteUsage = parsed.remoteUsage;
15
33
  if (typeof parsed.stackWithDefault === 'boolean')
16
34
  config.stackWithDefault = parsed.stackWithDefault;
35
+ if (parsed.border) {
36
+ const border = normalizeBorderStyle(parsed.border);
37
+ if (border)
38
+ config.border = border;
39
+ }
17
40
  }
18
41
  }
19
42
  catch { /* no config file — use defaults */ }
20
43
  if (envSegments)
21
44
  config.segments = envSegments.split(',');
45
+ if (envBorder) {
46
+ const border = normalizeBorderStyle(envBorder);
47
+ if (border)
48
+ config.border = border;
49
+ }
22
50
  return config;
23
51
  }
package/dist/layout.d.ts CHANGED
@@ -1,3 +1,19 @@
1
- import type { AgyStatusData, StatusBlocksConfig } from './types.js';
1
+ import type { Block, AgyStatusData, StatusBlocksConfig, BorderStyle } from './types.js';
2
2
  export declare const DEFAULT_ORDER: string[];
3
+ export interface BorderGlyphs {
4
+ tl: string;
5
+ tr: string;
6
+ bl: string;
7
+ br: string;
8
+ h: string;
9
+ v: string;
10
+ }
11
+ export declare const BORDER_STYLES: Record<BorderStyle, BorderGlyphs>;
12
+ type RowGroup = {
13
+ blocks: Block[];
14
+ widths: number[];
15
+ };
16
+ export declare function boxify(lines: string[], innerW: number, style?: BorderStyle, title?: string): string[];
17
+ export declare function renderRow(group: RowGroup, style?: BorderStyle): string[];
3
18
  export declare function render(data: AgyStatusData, width: number, config?: StatusBlocksConfig): string;
19
+ export {};
package/dist/layout.js CHANGED
@@ -7,35 +7,62 @@ import { gitSegment } from './segments/git.js';
7
7
  const ALL_SEGMENTS = [contextSegment, modelSegment, usageSegment, stateSegment, gitSegment];
8
8
  export const DEFAULT_ORDER = ['context', 'model', 'usage', 'state', 'git'];
9
9
  const GAP = 1;
10
- const BOX_CHROME = 4; // ╭─ ... ─╮ has 2 border chars + 2 inner spaces
11
- function boxify(lines, innerW, title) {
10
+ export const BORDER_STYLES = {
11
+ rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
12
+ square: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
13
+ double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
14
+ thick: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' },
15
+ ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' },
16
+ none: { tl: '', tr: '', bl: '', br: '', h: '', v: '' },
17
+ };
18
+ export function boxify(lines, innerW, style = 'rounded', title) {
19
+ if (style === 'none') {
20
+ const boxed = [];
21
+ if (title) {
22
+ boxed.push(' ' + color(title, c.dim) + ' '.repeat(Math.max(0, innerW + 1 - title.length)));
23
+ }
24
+ for (const line of lines) {
25
+ boxed.push(' ' + padRight(line, innerW) + ' ');
26
+ }
27
+ return boxed;
28
+ }
29
+ const g = BORDER_STYLES[style] || BORDER_STYLES.rounded;
12
30
  let top;
13
31
  if (title) {
14
32
  const label = ` ${title} `;
15
33
  const remaining = Math.max(0, innerW + 2 - label.length - 1);
16
- top = color('╭─', c.gray) + color(label, c.dim) + color('─'.repeat(remaining) + '╮', c.gray);
34
+ top = color(g.tl + g.h, c.gray) + color(label, c.dim) + color(g.h.repeat(remaining) + g.tr, c.gray);
17
35
  }
18
36
  else {
19
- top = color('╭' + '─'.repeat(innerW + 2) + '╮', c.gray);
37
+ top = color(g.tl + g.h.repeat(innerW + 2) + g.tr, c.gray);
20
38
  }
21
- const bot = color('╰' + '─'.repeat(innerW + 2) + '╯', c.gray);
39
+ const bot = color(g.bl + g.h.repeat(innerW + 2) + g.br, c.gray);
22
40
  const boxed = [top];
23
41
  for (const line of lines) {
24
- boxed.push(color('│', c.gray) + ' ' + padRight(line, innerW) + ' ' + color('│', c.gray));
42
+ boxed.push(color(g.v, c.gray) + ' ' + padRight(line, innerW) + ' ' + color(g.v, c.gray));
25
43
  }
26
44
  boxed.push(bot);
27
45
  return boxed;
28
46
  }
29
- function renderRow(group) {
47
+ export function renderRow(group, style = 'rounded') {
30
48
  const { blocks, widths } = group;
31
- const boxes = blocks.map((b, i) => boxify(b.lines, widths[i], b.id));
32
- const boxWidths = widths.map(w => w + BOX_CHROME);
49
+ const chrome = style === 'none' ? 2 : 4;
50
+ const boxes = blocks.map((b, i) => boxify(b.lines, widths[i], style, b.id));
51
+ const boxWidths = widths.map(w => w + chrome);
52
+ const g = BORDER_STYLES[style] || BORDER_STYLES.rounded;
33
53
  const maxHeight = Math.max(...boxes.map(b => b.length));
34
54
  for (let i = 0; i < boxes.length; i++) {
35
55
  const box = boxes[i];
36
56
  while (box.length < maxHeight) {
37
- const blankRow = color('│', c.gray) + ' '.repeat(widths[i] + 2) + color('│', c.gray);
38
- box.splice(box.length - 1, 0, blankRow);
57
+ const blankRow = style === 'none'
58
+ ? ' '.repeat(widths[i] + 2)
59
+ : color(g.v, c.gray) + ' '.repeat(widths[i] + 2) + color(g.v, c.gray);
60
+ if (style === 'none') {
61
+ box.push(blankRow);
62
+ }
63
+ else {
64
+ box.splice(box.length - 1, 0, blankRow);
65
+ }
39
66
  }
40
67
  }
41
68
  const rows = [];
@@ -63,12 +90,12 @@ function decodeAssignment(combo, blockCount, targetRows, blockRow, rowUsed) {
63
90
  }
64
91
  return true;
65
92
  }
66
- function computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths) {
93
+ function computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths, chrome) {
67
94
  rowWidths.fill(0, 0, targetRows);
68
95
  const rowCounts = new Array(targetRows).fill(0);
69
96
  for (let b = 0; b < blockCount; b++) {
70
97
  const r = blockRow[b];
71
- rowWidths[r] = (rowWidths[r] ?? 0) + (blockWidths[b] ?? 0) + BOX_CHROME;
98
+ rowWidths[r] = (rowWidths[r] ?? 0) + (blockWidths[b] ?? 0) + chrome;
72
99
  rowCounts[r] = (rowCounts[r] ?? 0) + 1;
73
100
  }
74
101
  for (let r = 0; r < targetRows; r++) {
@@ -77,10 +104,10 @@ function computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidt
77
104
  }
78
105
  }
79
106
  }
80
- function solveGreedy(blocks, maxWidth) {
107
+ function solveGreedy(blocks, maxWidth, chrome) {
81
108
  const rows = [];
82
109
  for (const b of blocks) {
83
- const boxW = b.width + BOX_CHROME;
110
+ const boxW = b.width + chrome;
84
111
  let placed = false;
85
112
  for (const r of rows) {
86
113
  const addedW = r.blocks.length > 0 ? boxW + GAP : boxW;
@@ -99,12 +126,12 @@ function solveGreedy(blocks, maxWidth) {
99
126
  rows.sort((a, b) => a.currentW - b.currentW);
100
127
  return rows.map(r => ({ blocks: r.blocks, widths: r.widths }));
101
128
  }
102
- function solveLayout(blocks, maxWidth) {
129
+ function solveLayout(blocks, maxWidth, chrome) {
103
130
  if (blocks.length === 0)
104
131
  return [];
105
132
  const blockCount = blocks.length;
106
133
  if (blockCount > 6)
107
- return solveGreedy(blocks, maxWidth);
134
+ return solveGreedy(blocks, maxWidth, chrome);
108
135
  const blockWidths = blocks.map(b => b.width);
109
136
  for (let targetRows = 1; targetRows <= blockCount; targetRows++) {
110
137
  const totalCombos = Math.pow(targetRows, blockCount);
@@ -116,7 +143,7 @@ function solveLayout(blocks, maxWidth) {
116
143
  for (let combo = 0; combo < totalCombos; combo++) {
117
144
  if (!decodeAssignment(combo, blockCount, targetRows, blockRow, rowUsed))
118
145
  continue;
119
- computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths);
146
+ computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths, chrome);
120
147
  let fits = true;
121
148
  let maxRowW = 0;
122
149
  for (let r = 0; r < targetRows; r++) {
@@ -145,7 +172,7 @@ function solveLayout(blocks, maxWidth) {
145
172
  rWidths.push(blocks[b].width);
146
173
  }
147
174
  }
148
- const totalW = rWidths.reduce((a, w) => a + w + BOX_CHROME, 0) + (rBlocks.length - 1) * GAP;
175
+ const totalW = rWidths.reduce((a, w) => a + w + chrome, 0) + (rBlocks.length - 1) * GAP;
149
176
  rows.push({ blocks: rBlocks, widths: rWidths, totalW });
150
177
  }
151
178
  rows.sort((a, b) => a.totalW - b.totalW);
@@ -156,15 +183,17 @@ function solveLayout(blocks, maxWidth) {
156
183
  }
157
184
  export function render(data, width, config = {}) {
158
185
  const order = config.segments ?? DEFAULT_ORDER;
186
+ const borderStyle = config.border ?? 'rounded';
187
+ const chrome = borderStyle === 'none' ? 2 : 4;
159
188
  const segmentsMap = new Map(ALL_SEGMENTS.map(s => [s.id, s]));
160
189
  const activeSegments = order
161
190
  .map(id => segmentsMap.get(id))
162
191
  .filter((s) => !!s && s.enabled(data));
163
192
  const blocks = activeSegments.map(s => s.render(data, width));
164
- const groups = solveLayout(blocks, width);
193
+ const groups = solveLayout(blocks, width, chrome);
165
194
  const lines = [];
166
195
  for (const group of groups) {
167
- lines.push(...renderRow(group));
196
+ lines.push(...renderRow(group, borderStyle));
168
197
  }
169
198
  return lines.join('\n');
170
199
  }
@@ -178,12 +178,34 @@ export const usageSegment = {
178
178
  return { id: 'usage', priority: 25, width: 0, lines: [''] };
179
179
  }
180
180
  const BAR_WIDTH = 8;
181
- const rows = limits.slice(0, 2).map(l => {
182
- const bar = renderBar(l.percent, BAR_WIDTH);
183
- const pctStr = color(`${l.percent}%`, pctColor(l.percent), c.bold);
184
- const reset = l.resetsAt ? color(`↻ ${formatResetTime(l.resetsAt)}`, c.dim) : '';
185
- const lbl = color(l.label, c.dim);
186
- const parts = [bar, pctStr, reset, lbl].filter(Boolean);
181
+ const active = limits.slice(0, 2);
182
+ const formatted = active.map(l => {
183
+ const pctStr = `${l.percent}%`;
184
+ const resetTime = l.resetsAt ? formatResetTime(l.resetsAt) : '';
185
+ const resetRaw = resetTime ? `↻ ${resetTime}` : '';
186
+ return {
187
+ limit: l,
188
+ pctStr,
189
+ resetRaw,
190
+ label: l.label,
191
+ };
192
+ });
193
+ const maxPctLen = Math.max(...formatted.map(r => r.pctStr.length));
194
+ const hasAnyReset = formatted.some(r => r.resetRaw.length > 0);
195
+ const maxResetLen = hasAnyReset ? Math.max(...formatted.map(r => r.resetRaw.length)) : 0;
196
+ const maxLabelLen = Math.max(...formatted.map(r => r.label.length));
197
+ const rows = formatted.map(r => {
198
+ const bar = renderBar(r.limit.percent, BAR_WIDTH);
199
+ const paddedPct = ' '.repeat(maxPctLen - r.pctStr.length) + r.pctStr;
200
+ const pctColorStr = color(paddedPct, pctColor(r.limit.percent), c.bold);
201
+ let resetColorStr = '';
202
+ if (hasAnyReset && r.resetRaw) {
203
+ const paddedReset = r.resetRaw + ' '.repeat(maxResetLen - r.resetRaw.length);
204
+ resetColorStr = color(paddedReset, c.dim);
205
+ }
206
+ const paddedLabel = ' '.repeat(maxLabelLen - r.label.length) + r.label;
207
+ const lbl = color(paddedLabel, c.dim);
208
+ const parts = [bar, pctColorStr, resetColorStr, lbl].filter(Boolean);
187
209
  return parts.join(color(' · ', c.dim));
188
210
  });
189
211
  const width = Math.max(...rows.map(visibleLength));
package/dist/types.d.ts CHANGED
@@ -76,8 +76,10 @@ export interface Segment {
76
76
  render(data: AgyStatusData, allocWidth: number): Block;
77
77
  enabled(data: AgyStatusData): boolean;
78
78
  }
79
+ export type BorderStyle = 'rounded' | 'square' | 'double' | 'thick' | 'ascii' | 'none';
79
80
  export interface StatusBlocksConfig {
80
81
  segments?: string[];
81
82
  stackWithDefault?: boolean;
82
83
  remoteUsage?: boolean;
84
+ border?: BorderStyle;
83
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-statusblocks",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Opinionated, adaptive block-based status line for Antigravity CLI (agy) with bin-packing layout, rate limit tracking, and live agent telemetry",
5
5
  "type": "module",
6
6
  "bin": {