agy-statusblocks 0.0.0-stage → 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { loadConfig } from './config.js';
7
+ import { render } from './layout.js';
8
+ import { maybeRefreshRemoteUsage } from './remote-usage.js';
9
+ import { spawnDetached } from './spawn.js';
10
+ const AUTO_UPDATE_INTERVAL = 86400000; // 24 hours
11
+ function maybeAutoUpdate() {
12
+ if (process.env['AGY_STATUSBLOCKS_NO_UPDATE'])
13
+ return;
14
+ try {
15
+ const dir = join(homedir(), '.gemini', 'antigravity-cli', 'statusblocks');
16
+ if (!existsSync(dir))
17
+ return;
18
+ const checkFile = join(dir, '.last-update-check');
19
+ let lastCheck = 0;
20
+ try {
21
+ lastCheck = parseInt(readFileSync(checkFile, 'utf8'), 10) || 0;
22
+ }
23
+ catch { /* missing file */ }
24
+ if (Date.now() - lastCheck < AUTO_UPDATE_INTERVAL)
25
+ return;
26
+ writeFileSync(checkFile, String(Date.now()));
27
+ spawnDetached(process.execPath, [join(dir, 'update-check.js')]);
28
+ }
29
+ catch { /* auto-update is best-effort */ }
30
+ }
31
+ function detectWidth(providedWidth) {
32
+ if (providedWidth && providedWidth > 0)
33
+ return providedWidth;
34
+ let w = process.stderr.columns || process.stdout.columns || 0;
35
+ if (!w)
36
+ w = parseInt(process.env.COLUMNS ?? '', 10) || 0;
37
+ if (w > 0)
38
+ return w;
39
+ if (process.platform !== 'win32') {
40
+ try {
41
+ w = parseInt(execSync('tput cols', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(), 10) || 0;
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ }
47
+ return w > 0 ? w : 120;
48
+ }
49
+ function coerceNumber(v) {
50
+ if (typeof v === 'number' && Number.isFinite(v))
51
+ return v;
52
+ if (typeof v === 'string') {
53
+ const parsed = parseFloat(v.replace(/%/g, '').trim());
54
+ if (Number.isFinite(parsed))
55
+ return parsed;
56
+ }
57
+ return undefined;
58
+ }
59
+ function normalizeInput(raw) {
60
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
61
+ return null;
62
+ const obj = raw;
63
+ const modelObj = (obj.model && typeof obj.model === 'object') ? obj.model : {};
64
+ const modelName = typeof modelObj.display_name === 'string'
65
+ ? modelObj.display_name
66
+ : typeof modelObj.id === 'string'
67
+ ? modelObj.id
68
+ : 'Gemini';
69
+ const cwObj = (obj.context_window && typeof obj.context_window === 'object') ? obj.context_window : {};
70
+ const cwSize = coerceNumber(cwObj.context_window_size) ?? 1048576;
71
+ const usedPct = coerceNumber(cwObj.used_percentage) ?? 0;
72
+ const remPct = coerceNumber(cwObj.remaining_percentage) ?? (100 - usedPct);
73
+ const cwd = typeof obj.current_dir === 'string'
74
+ ? obj.current_dir
75
+ : (typeof obj.workspace === 'object' && obj.workspace && typeof obj.workspace.current_dir === 'string')
76
+ ? obj.workspace.current_dir
77
+ : process.cwd();
78
+ const vcs = (obj.vcs && typeof obj.vcs === 'object') ? obj.vcs : undefined;
79
+ let effortLevel = typeof modelObj.effort === 'string' ? modelObj.effort.toLowerCase() : undefined;
80
+ if (!effortLevel) {
81
+ if (modelName.includes('(High)'))
82
+ effortLevel = 'high';
83
+ else if (modelName.includes('(Medium)'))
84
+ effortLevel = 'medium';
85
+ else if (modelName.includes('(Low)'))
86
+ effortLevel = 'low';
87
+ else if (modelName.includes('(Max)'))
88
+ effortLevel = 'max';
89
+ }
90
+ const costObj = (obj.cost && typeof obj.cost === 'object') ? obj.cost : {};
91
+ const rlObj = (obj.rate_limits && typeof obj.rate_limits === 'object') ? obj.rate_limits : undefined;
92
+ const quotaObj = (obj.quota && typeof obj.quota === 'object') ? obj.quota : undefined;
93
+ return {
94
+ product: typeof obj.product === 'string' ? obj.product : 'antigravity',
95
+ conversation_id: typeof obj.conversation_id === 'string' ? obj.conversation_id : undefined,
96
+ agent_state: typeof obj.agent_state === 'string' ? obj.agent_state : 'idle',
97
+ model: {
98
+ id: typeof modelObj.id === 'string' ? modelObj.id : 'gemini',
99
+ display_name: modelName,
100
+ effort: effortLevel,
101
+ },
102
+ context_window: {
103
+ context_window_size: cwSize,
104
+ used_percentage: usedPct,
105
+ remaining_percentage: remPct,
106
+ total_input_tokens: coerceNumber(cwObj.total_input_tokens) ?? 0,
107
+ total_output_tokens: coerceNumber(cwObj.total_output_tokens) ?? 0,
108
+ },
109
+ rate_limits: rlObj,
110
+ quota: quotaObj,
111
+ current_dir: cwd,
112
+ workspace: { current_dir: cwd },
113
+ vcs: vcs ? {
114
+ type: typeof vcs.type === 'string' ? vcs.type : 'git',
115
+ branch: typeof vcs.branch === 'string' ? vcs.branch : undefined,
116
+ dirty: typeof vcs.dirty === 'boolean' ? vcs.dirty : undefined,
117
+ } : undefined,
118
+ artifact_count: coerceNumber(obj.artifact_count) ?? 0,
119
+ task_count: coerceNumber(obj.task_count) ?? 0,
120
+ subagents: Array.isArray(obj.subagents) ? obj.subagents : [],
121
+ plan_tier: typeof obj.plan_tier === 'string' ? obj.plan_tier : 'Consumer',
122
+ terminal_width: coerceNumber(obj.terminal_width),
123
+ cost: {
124
+ total_usd: coerceNumber(costObj.total_usd),
125
+ subagent_usd: coerceNumber(costObj.subagent_usd),
126
+ estimated: typeof costObj.estimated === 'boolean' ? costObj.estimated : coerceNumber(costObj.estimated),
127
+ },
128
+ effort: effortLevel ? { level: effortLevel } : undefined,
129
+ };
130
+ }
131
+ let input = '';
132
+ process.stdin.setEncoding('utf8');
133
+ process.stdin.on('data', (chunk) => { input += chunk; });
134
+ process.stdin.on('end', () => {
135
+ try {
136
+ if (!input.trim()) {
137
+ process.stdout.write('\n');
138
+ return;
139
+ }
140
+ const raw = JSON.parse(input);
141
+ const data = normalizeInput(raw);
142
+ if (!data) {
143
+ process.stdout.write('\n');
144
+ return;
145
+ }
146
+ const config = loadConfig();
147
+ if (config.remoteUsage === true) {
148
+ maybeRefreshRemoteUsage();
149
+ }
150
+ const width = detectWidth(data.terminal_width);
151
+ const rendered = render(data, width, config);
152
+ process.stdout.write(rendered + '\n');
153
+ maybeAutoUpdate();
154
+ }
155
+ catch {
156
+ process.stdout.write('\n');
157
+ }
158
+ });
@@ -0,0 +1,3 @@
1
+ import type { AgyStatusData, StatusBlocksConfig } from './types.js';
2
+ export declare const DEFAULT_ORDER: string[];
3
+ export declare function render(data: AgyStatusData, width: number, config?: StatusBlocksConfig): string;
package/dist/layout.js ADDED
@@ -0,0 +1,146 @@
1
+ import { padRight, color, c } from './colors.js';
2
+ import { contextSegment } from './segments/context.js';
3
+ import { modelSegment } from './segments/model.js';
4
+ import { usageSegment } from './segments/usage.js';
5
+ import { stateSegment } from './segments/state.js';
6
+ import { gitSegment } from './segments/git.js';
7
+ const ALL_SEGMENTS = [contextSegment, modelSegment, usageSegment, stateSegment, gitSegment];
8
+ export const DEFAULT_ORDER = ['context', 'model', 'usage', 'state', 'git'];
9
+ const GAP = 1;
10
+ const BOX_CHROME = 4; // ╭─ ... ─╮ has 2 border chars + 2 inner spaces
11
+ function boxify(lines, innerW, title) {
12
+ let top;
13
+ if (title) {
14
+ const label = ` ${title} `;
15
+ const remaining = Math.max(0, innerW + 2 - label.length - 1);
16
+ top = color('╭─', c.gray) + color(label, c.dim) + color('─'.repeat(remaining) + '╮', c.gray);
17
+ }
18
+ else {
19
+ top = color('╭' + '─'.repeat(innerW + 2) + '╮', c.gray);
20
+ }
21
+ const bot = color('╰' + '─'.repeat(innerW + 2) + '╯', c.gray);
22
+ const boxed = [top];
23
+ for (const line of lines) {
24
+ boxed.push(color('│', c.gray) + ' ' + padRight(line, innerW) + ' ' + color('│', c.gray));
25
+ }
26
+ boxed.push(bot);
27
+ return boxed;
28
+ }
29
+ function renderRow(group) {
30
+ 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);
33
+ const maxHeight = Math.max(...boxes.map(b => b.length));
34
+ for (let i = 0; i < boxes.length; i++) {
35
+ const box = boxes[i];
36
+ 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);
39
+ }
40
+ }
41
+ const rows = [];
42
+ for (let row = 0; row < maxHeight; row++) {
43
+ const parts = [];
44
+ for (let i = 0; i < boxes.length; i++) {
45
+ parts.push(padRight(boxes[i][row], boxWidths[i]));
46
+ }
47
+ rows.push(parts.join(' '.repeat(GAP)).replace(/\s+$/, ''));
48
+ }
49
+ return rows;
50
+ }
51
+ function decodeAssignment(combo, blockCount, targetRows, blockRow, rowUsed) {
52
+ let encoded = combo;
53
+ for (let b = 0; b < blockCount; b++) {
54
+ blockRow[b] = encoded % targetRows;
55
+ encoded = Math.floor(encoded / targetRows);
56
+ }
57
+ rowUsed.fill(false, 0, targetRows);
58
+ for (let b = 0; b < blockCount; b++)
59
+ rowUsed[blockRow[b]] = true;
60
+ for (let r = 0; r < targetRows; r++) {
61
+ if (!rowUsed[r])
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ function computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths) {
67
+ rowWidths.fill(0, 0, targetRows);
68
+ const rowCounts = new Array(targetRows).fill(0);
69
+ for (let b = 0; b < blockCount; b++) {
70
+ const r = blockRow[b];
71
+ rowWidths[r] = (rowWidths[r] ?? 0) + (blockWidths[b] ?? 0) + BOX_CHROME;
72
+ rowCounts[r] = (rowCounts[r] ?? 0) + 1;
73
+ }
74
+ for (let r = 0; r < targetRows; r++) {
75
+ if ((rowCounts[r] ?? 0) > 1) {
76
+ rowWidths[r] = (rowWidths[r] ?? 0) + ((rowCounts[r] ?? 0) - 1) * GAP;
77
+ }
78
+ }
79
+ }
80
+ function solveLayout(blocks, maxWidth) {
81
+ if (blocks.length === 0)
82
+ return [];
83
+ const blockCount = blocks.length;
84
+ const blockWidths = blocks.map(b => b.width);
85
+ for (let targetRows = 1; targetRows <= blockCount; targetRows++) {
86
+ const totalCombos = Math.pow(targetRows, blockCount);
87
+ const blockRow = new Array(blockCount);
88
+ const rowUsed = new Array(targetRows);
89
+ const rowWidths = new Array(targetRows);
90
+ let bestCombo = -1;
91
+ let minMaxRowW = Infinity;
92
+ for (let combo = 0; combo < totalCombos; combo++) {
93
+ if (!decodeAssignment(combo, blockCount, targetRows, blockRow, rowUsed))
94
+ continue;
95
+ computeRowWidths(blockRow, blockCount, targetRows, blockWidths, rowWidths);
96
+ let fits = true;
97
+ let maxRowW = 0;
98
+ for (let r = 0; r < targetRows; r++) {
99
+ const rw = rowWidths[r];
100
+ if (rw > maxWidth) {
101
+ fits = false;
102
+ break;
103
+ }
104
+ if (rw > maxRowW)
105
+ maxRowW = rw;
106
+ }
107
+ if (fits && maxRowW < minMaxRowW) {
108
+ minMaxRowW = maxRowW;
109
+ bestCombo = combo;
110
+ }
111
+ }
112
+ if (bestCombo !== -1) {
113
+ decodeAssignment(bestCombo, blockCount, targetRows, blockRow, rowUsed);
114
+ const rows = [];
115
+ for (let r = 0; r < targetRows; r++) {
116
+ const rBlocks = [];
117
+ const rWidths = [];
118
+ for (let b = 0; b < blockCount; b++) {
119
+ if (blockRow[b] === r) {
120
+ rBlocks.push(blocks[b]);
121
+ rWidths.push(blocks[b].width);
122
+ }
123
+ }
124
+ const totalW = rWidths.reduce((a, w) => a + w + BOX_CHROME, 0) + (rBlocks.length - 1) * GAP;
125
+ rows.push({ blocks: rBlocks, widths: rWidths, totalW });
126
+ }
127
+ rows.sort((a, b) => a.totalW - b.totalW);
128
+ return rows.map(r => ({ blocks: r.blocks, widths: r.widths }));
129
+ }
130
+ }
131
+ return blocks.map(b => ({ blocks: [b], widths: [b.width] }));
132
+ }
133
+ export function render(data, width, config = {}) {
134
+ const order = config.segments ?? DEFAULT_ORDER;
135
+ const segmentsMap = new Map(ALL_SEGMENTS.map(s => [s.id, s]));
136
+ const activeSegments = order
137
+ .map(id => segmentsMap.get(id))
138
+ .filter((s) => !!s && s.enabled(data));
139
+ const blocks = activeSegments.map(s => s.render(data, width));
140
+ const groups = solveLayout(blocks, width);
141
+ const lines = [];
142
+ for (const group of groups) {
143
+ lines.push(...renderRow(group));
144
+ }
145
+ return lines.join('\n');
146
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { getOAuthToken } from './credentials.js';
3
+ import { normalizeLimits, storeRemoteLimits } from './remote-usage.js';
4
+ async function main() {
5
+ const token = getOAuthToken();
6
+ if (!token)
7
+ return;
8
+ const res = await fetch('https://api.anthropic.com/api/oauth/usage', {
9
+ headers: {
10
+ 'Authorization': `Bearer ${token}`,
11
+ 'anthropic-beta': 'oauth-2025-04-20',
12
+ },
13
+ signal: AbortSignal.timeout(10000),
14
+ });
15
+ if (!res.ok)
16
+ return;
17
+ const body = (await res.json());
18
+ storeRemoteLimits(normalizeLimits(body?.limits));
19
+ }
20
+ main().catch(() => {
21
+ // Silent fallback
22
+ });
@@ -0,0 +1,14 @@
1
+ export interface RemoteLimit {
2
+ label: string;
3
+ percent: number;
4
+ resetsAt: number;
5
+ scope?: 'model' | 'surface';
6
+ match?: string;
7
+ id?: string;
8
+ }
9
+ export declare const REMOTE_USAGE_CACHE = "remote-usage";
10
+ export declare const KIND_LABELS: Record<string, string>;
11
+ export declare function normalizeLimits(raw: unknown): RemoteLimit[];
12
+ export declare function storeRemoteLimits(limits: RemoteLimit[]): void;
13
+ export declare function readRemoteLimits(): RemoteLimit[] | null;
14
+ export declare function maybeRefreshRemoteUsage(): void;
@@ -0,0 +1,93 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { join, dirname } from 'path';
3
+ import { readCacheEntry, writeCache, acquireMarker } from './cache.js';
4
+ import { spawnDetached } from './spawn.js';
5
+ export const REMOTE_USAGE_CACHE = 'remote-usage';
6
+ const REFRESH_THROTTLE_MARKER = 'remote-usage-attempt';
7
+ const DISPLAY_TTL = 3600000; // 1 hour
8
+ const REFRESH_INTERVAL = 60000; // 1 minute
9
+ export const KIND_LABELS = {
10
+ session: '5h',
11
+ weekly_all: '7d',
12
+ };
13
+ const KNOWN_GENERIC_KINDS = new Set(Object.keys(KIND_LABELS));
14
+ function clampPercent(v) {
15
+ if (typeof v !== 'number' || !Number.isFinite(v))
16
+ return null;
17
+ return Math.min(999, Math.max(0, Math.round(v)));
18
+ }
19
+ export function normalizeLimits(raw) {
20
+ if (!Array.isArray(raw))
21
+ return [];
22
+ const out = [];
23
+ for (const entry of raw) {
24
+ if (!entry || typeof entry !== 'object')
25
+ continue;
26
+ const e = entry;
27
+ const percent = clampPercent(e.percent);
28
+ if (percent === null)
29
+ continue;
30
+ let resetsAt = 0;
31
+ if (typeof e.resets_at === 'string') {
32
+ const ms = Date.parse(e.resets_at);
33
+ if (Number.isFinite(ms))
34
+ resetsAt = Math.floor(ms / 1000);
35
+ }
36
+ const kind = typeof e.kind === 'string' ? e.kind : '';
37
+ if (KNOWN_GENERIC_KINDS.has(kind)) {
38
+ out.push({ label: KIND_LABELS[kind], percent, resetsAt });
39
+ continue;
40
+ }
41
+ const scope = e.scope;
42
+ const modelName = typeof scope?.model?.display_name === 'string' ? scope.model.display_name : '';
43
+ const modelId = typeof scope?.model?.id === 'string' ? scope.model.id : '';
44
+ const surface = typeof scope?.surface === 'string' ? scope.surface : '';
45
+ if (surface)
46
+ continue;
47
+ if (modelName || modelId) {
48
+ out.push({
49
+ label: modelName || modelId,
50
+ percent,
51
+ resetsAt,
52
+ scope: 'model',
53
+ match: (modelName || modelId).toLowerCase(),
54
+ id: modelId || undefined,
55
+ });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ export function storeRemoteLimits(limits) {
61
+ writeCache(REMOTE_USAGE_CACHE, limits);
62
+ }
63
+ function remoteUsageDisabled() {
64
+ return !!(process.env['AGY_STATUSBLOCKS_NO_REMOTE'] || process.env['CLAUDE_STATUSBLOCKS_NO_REMOTE']);
65
+ }
66
+ export function readRemoteLimits() {
67
+ if (remoteUsageDisabled())
68
+ return null;
69
+ const entry = readCacheEntry(REMOTE_USAGE_CACHE);
70
+ if (!entry || Date.now() - entry.ts > DISPLAY_TTL)
71
+ return null;
72
+ if (!Array.isArray(entry.value))
73
+ return null;
74
+ return entry.value;
75
+ }
76
+ export function maybeRefreshRemoteUsage() {
77
+ if (remoteUsageDisabled())
78
+ return;
79
+ const entry = readCacheEntry(REMOTE_USAGE_CACHE);
80
+ const isFresh = entry && Date.now() - entry.ts < REFRESH_INTERVAL;
81
+ if (isFresh)
82
+ return;
83
+ if (!acquireMarker(REFRESH_THROTTLE_MARKER, REFRESH_INTERVAL))
84
+ return;
85
+ try {
86
+ const here = dirname(fileURLToPath(import.meta.url));
87
+ const script = join(here, 'remote-usage-fetch.js');
88
+ spawnDetached(process.execPath, [script]);
89
+ }
90
+ catch {
91
+ // Best-effort
92
+ }
93
+ }
@@ -0,0 +1,2 @@
1
+ import type { Segment } from '../types.js';
2
+ export declare const contextSegment: Segment;
@@ -0,0 +1,30 @@
1
+ import { color, c, visibleLength, pctColor, renderBar } from '../colors.js';
2
+ function formatTokens(n) {
3
+ if (n >= 1000000)
4
+ return `${(n / 1000000).toFixed(1)}M`;
5
+ if (n >= 1000)
6
+ return `${Math.round(n / 1000)}K`;
7
+ return String(n);
8
+ }
9
+ export const contextSegment = {
10
+ id: 'context',
11
+ priority: 10,
12
+ enabled: () => true,
13
+ render(data) {
14
+ const total = data.context_window.context_window_size || 1048576;
15
+ let pct = data.context_window.used_percentage != null
16
+ ? Math.floor(data.context_window.used_percentage)
17
+ : 0;
18
+ const inTokens = data.context_window.total_input_tokens ?? 0;
19
+ const outTokens = data.context_window.total_output_tokens ?? 0;
20
+ const used = inTokens + outTokens;
21
+ if (pct === 0 && used > 0 && total > 0) {
22
+ pct = Math.floor((used / total) * 100);
23
+ }
24
+ const barColor = pctColor(pct);
25
+ const info = `${color(`${pct}%`, barColor, c.bold)}${color(' · ', c.dim)}${color(`${formatTokens(used)}/${formatTokens(total)}`, c.dim)}`;
26
+ const cardWidth = Math.max(16, visibleLength(info));
27
+ const lines = [renderBar(pct, cardWidth), info];
28
+ return { id: 'context', priority: 10, width: cardWidth, lines };
29
+ },
30
+ };
@@ -0,0 +1,2 @@
1
+ import type { Segment } from '../types.js';
2
+ export declare const gitSegment: Segment;
@@ -0,0 +1,95 @@
1
+ import { execSync } from 'child_process';
2
+ import { color, c, visibleLength } from '../colors.js';
3
+ import { readCache, writeCache, hashKey } from '../cache.js';
4
+ const GIT_CACHE_TTL = 4000;
5
+ let memo = null;
6
+ function defang(s) {
7
+ return s.replace(/[^\x20-\x7e\u00a0-\uffff]/g, '');
8
+ }
9
+ function normalize(v) {
10
+ if (!v || typeof v !== 'object')
11
+ return null;
12
+ const o = v;
13
+ if (typeof o.branch !== 'string')
14
+ return null;
15
+ return {
16
+ branch: defang(o.branch),
17
+ staged: Number(o.staged) || 0,
18
+ modified: Number(o.modified) || 0,
19
+ added: Number(o.added) || 0,
20
+ removed: Number(o.removed) || 0,
21
+ };
22
+ }
23
+ function getGitInfo(cwd) {
24
+ if (memo && memo.cwd === cwd)
25
+ return memo.info;
26
+ const cacheName = `git-${hashKey(cwd)}`;
27
+ const cached = readCache(cacheName, GIT_CACHE_TTL);
28
+ if (cached !== undefined) {
29
+ memo = { cwd, info: normalize(cached) };
30
+ return memo.info;
31
+ }
32
+ let info = null;
33
+ try {
34
+ let branch = execSync('git branch --show-current', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
35
+ if (!branch) {
36
+ branch = '@' + execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
37
+ }
38
+ const stagedOut = execSync('git diff --cached --numstat', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
39
+ const modifiedOut = execSync('git diff --numstat', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
40
+ const staged = stagedOut ? stagedOut.split('\n').length : 0;
41
+ const modified = modifiedOut ? modifiedOut.split('\n').length : 0;
42
+ let added = 0, removed = 0;
43
+ for (const line of [...(stagedOut ? stagedOut.split('\n') : []), ...(modifiedOut ? modifiedOut.split('\n') : [])]) {
44
+ const [a, r] = line.split('\t');
45
+ if (a && a !== '-')
46
+ added += parseInt(a, 10) || 0;
47
+ if (r && r !== '-')
48
+ removed += parseInt(r, 10) || 0;
49
+ }
50
+ info = normalize({ branch, staged, modified, added, removed });
51
+ }
52
+ catch {
53
+ info = null;
54
+ }
55
+ memo = { cwd, info };
56
+ writeCache(cacheName, info);
57
+ return info;
58
+ }
59
+ export const gitSegment = {
60
+ id: 'git',
61
+ priority: 40,
62
+ enabled: (data) => {
63
+ if (data.vcs?.branch)
64
+ return true;
65
+ const cwd = data.workspace?.current_dir || data.current_dir || process.cwd();
66
+ const info = getGitInfo(cwd);
67
+ return info !== null && info.branch !== '';
68
+ },
69
+ render(data) {
70
+ const cwd = data.workspace?.current_dir || data.current_dir || process.cwd();
71
+ const info = getGitInfo(cwd);
72
+ const branch = data.vcs?.branch || info?.branch || 'git';
73
+ const line1 = color(branch, c.cyan);
74
+ let line2 = color('clean', c.dim);
75
+ if (info) {
76
+ const parts = [];
77
+ if (info.staged > 0)
78
+ parts.push(color(`${info.staged} staged`, c.green));
79
+ if (info.modified > 0)
80
+ parts.push(color(`${info.modified} modified`, c.yellow));
81
+ if (parts.length === 0)
82
+ parts.push(color('clean', c.dim));
83
+ if (info.added > 0 || info.removed > 0) {
84
+ parts.push(`${color(`+${info.added}`, c.green)} ${color(`-${info.removed}`, c.red)}`);
85
+ }
86
+ line2 = parts.join(color(' · ', c.dim));
87
+ }
88
+ else if (data.vcs?.dirty) {
89
+ line2 = color('modified', c.yellow);
90
+ }
91
+ const lines = [line1, line2];
92
+ const width = Math.max(...lines.map(visibleLength));
93
+ return { id: 'git', priority: 40, width, lines };
94
+ },
95
+ };
@@ -0,0 +1,2 @@
1
+ import type { Segment } from '../types.js';
2
+ export declare const modelSegment: Segment;
@@ -0,0 +1,96 @@
1
+ import { basename } from 'path';
2
+ import { homedir } from 'os';
3
+ import { color, c, visibleLength } from '../colors.js';
4
+ function tildeDir(dir) {
5
+ const home = homedir();
6
+ if (dir === home)
7
+ return '~';
8
+ if (dir.startsWith(home + '/') || dir.startsWith(home + '\\')) {
9
+ return '~' + dir.slice(home.length);
10
+ }
11
+ return dir;
12
+ }
13
+ const EFFORT_COLORS = {
14
+ low: c.dim,
15
+ medium: c.dim,
16
+ high: c.yellow,
17
+ xhigh: c.orange,
18
+ max: c.orange + c.bold,
19
+ };
20
+ function spreadLine(parts, targetWidth) {
21
+ if (parts.length === 0)
22
+ return '';
23
+ if (parts.length === 1)
24
+ return parts[0];
25
+ const dot = color(' · ', c.dim);
26
+ const dotWidth = 3;
27
+ const widths = parts.map(visibleLength);
28
+ const contentWidth = widths.reduce((a, b) => a + b, 0);
29
+ const totalGap = Math.max(0, targetWidth - contentWidth);
30
+ const spareAfterDots = totalGap - dotWidth * (parts.length - 1);
31
+ if (spareAfterDots <= 10) {
32
+ const gaps = parts.length * 2 - 2;
33
+ const base = Math.floor(Math.max(0, spareAfterDots) / gaps);
34
+ const remainder = Math.max(0, spareAfterDots) % gaps;
35
+ const result = [parts[0]];
36
+ for (let i = 1; i < parts.length; i++) {
37
+ const leftPad = base + (((i - 1) * 2) < remainder ? 1 : 0);
38
+ const rightPad = base + (((i - 1) * 2 + 1) < remainder ? 1 : 0);
39
+ result.push(' '.repeat(leftPad), dot, ' '.repeat(rightPad), parts[i]);
40
+ }
41
+ return result.join('');
42
+ }
43
+ const gapCount = parts.length - 1;
44
+ const baseGap = Math.floor(totalGap / gapCount);
45
+ const extraGap = totalGap % gapCount;
46
+ const result = [parts[0]];
47
+ for (let i = 1; i < parts.length; i++) {
48
+ result.push(' '.repeat(baseGap + (i <= extraGap ? 1 : 0)), parts[i]);
49
+ }
50
+ return result.join('');
51
+ }
52
+ export const modelSegment = {
53
+ id: 'model',
54
+ priority: 30,
55
+ enabled: () => true,
56
+ render(data, allocWidth) {
57
+ const fullName = data.model.display_name || data.model.id || 'Gemini';
58
+ const shortName = fullName.replace(/\s*\(.*?\)\s*/g, '').trim();
59
+ // Determine reasoning effort from display name or explicit effort
60
+ let effort = data.model.effort || data.effort?.level;
61
+ if (!effort) {
62
+ if (fullName.includes('(High)'))
63
+ effort = 'high';
64
+ else if (fullName.includes('(Medium)'))
65
+ effort = 'medium';
66
+ else if (fullName.includes('(Low)'))
67
+ effort = 'low';
68
+ else if (fullName.includes('(Max)'))
69
+ effort = 'max';
70
+ }
71
+ const currentDir = data.workspace?.current_dir || data.current_dir || process.cwd();
72
+ const tilde = tildeDir(currentDir);
73
+ const isClaude = /claude|sonnet|opus|haiku/i.test(shortName);
74
+ const nameStyled = color(shortName, isClaude ? c.orange : c.gemini, c.bold);
75
+ const sep = color(' · ', c.dim);
76
+ const nameLen = visibleLength(nameStyled) + visibleLength(sep);
77
+ const dirBudget = Math.max(allocWidth - nameLen, 0);
78
+ const dir = tilde.length <= dirBudget ? tilde : basename(currentDir);
79
+ const line1 = `${nameStyled}${sep}${color(dir, c.dim)}`;
80
+ const effortStr = effort ? color(effort.toLowerCase(), EFFORT_COLORS[effort.toLowerCase()] ?? c.dim) : '';
81
+ const tierStr = data.plan_tier ? color(data.plan_tier, c.dim) : '';
82
+ const usd = typeof data.cost?.total_usd === 'number'
83
+ ? data.cost.total_usd
84
+ : typeof data.cost?.estimated === 'number'
85
+ ? data.cost.estimated
86
+ : 0;
87
+ const costStr = usd > 0
88
+ ? color(`${data.cost?.estimated === true ? '~' : ''}$${usd.toFixed(3)}`, c.dim)
89
+ : '';
90
+ const parts = [effortStr, tierStr, costStr].filter(Boolean);
91
+ const line2 = spreadLine(parts, visibleLength(line1));
92
+ const lines = [line1, line2 || color(shortName, c.dim)];
93
+ const width = Math.max(...lines.map(visibleLength));
94
+ return { id: 'model', priority: 30, width, lines };
95
+ },
96
+ };