Skip to content

Commit dc94951

Browse files
feat(cli): add Antigravity CLI statusline support
Add the `oh-my-posh antigravity` command and matching segment, mirroring the existing Claude Code and GitHub Copilot CLI integrations: same workspace.current_dir/cwd fallback for directory-aware segments, same token-gauge template shape. Field set is based on Antigravity CLI's published statusline JSON schema. Closes #7806
1 parent 8259d4b commit dc94951

15 files changed

Lines changed: 991 additions & 12 deletions

File tree

src/cache/cache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
FONTLISTCACHE = "font_list_cache"
3434
CLAUDECACHE = "claude_cache"
3535
COPILOTCLICACHE = "copilot_cli_cache"
36+
ANTIGRAVITYCACHE = "antigravity_cache"
3637
)
3738

3839
type Entry[T any] struct {

src/cli/antigravity.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package cli
2+
3+
import (
4+
"github.com/jandedobbeleer/oh-my-posh/src/cache"
5+
"github.com/jandedobbeleer/oh-my-posh/src/config"
6+
"github.com/jandedobbeleer/oh-my-posh/src/segments"
7+
"github.com/jandedobbeleer/oh-my-posh/src/shell"
8+
9+
"github.com/jandedobbeleer/oh-my-posh/src/cmdtree"
10+
)
11+
12+
var antigravityCmd = &cmdtree.Command{
13+
Use: "antigravity",
14+
Short: "Render a prompt for Antigravity CLI statusline",
15+
Long: `Render a prompt for Antigravity CLI statusline integration.
16+
17+
This command reads Antigravity CLI's contextual JSON data from stdin and renders
18+
a prompt that can include an Antigravity segment with session information like
19+
model name, token usage, and more.
20+
21+
Example usage in Antigravity CLI settings (~/.gemini/antigravity-cli/settings.json):
22+
{
23+
"statusLine": {
24+
"type": "command",
25+
"command": "oh-my-posh antigravity --config ~/.config/ohmyposh/antigravity.toml"
26+
}
27+
}`,
28+
Args: cmdtree.NoArgs,
29+
Run: statuslineRun(
30+
shell.ANTIGRAVITY,
31+
cache.ANTIGRAVITYCACHE,
32+
func(d *segments.AntigravityData) string { return d.SessionID },
33+
antigravityPWD,
34+
config.Antigravity,
35+
),
36+
}
37+
38+
func init() {
39+
RootCmd.AddCommand(antigravityCmd)
40+
}
41+
42+
func antigravityPWD(d *segments.AntigravityData) string {
43+
return workingDirectory(d.Workspace.CurrentDir, d.CWD)
44+
}

src/cli/statusline_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,44 @@ func TestCopilotPWD(t *testing.T) {
134134
}
135135
}
136136

137+
func TestAntigravityPWD(t *testing.T) {
138+
cases := []struct {
139+
Case string
140+
Data *segments.AntigravityData
141+
Expected string
142+
}{
143+
{
144+
Case: "both set, disagreeing: current_dir wins",
145+
Data: &segments.AntigravityData{CWD: "/b", Workspace: segments.AntigravityWorkspace{CurrentDir: "/a"}},
146+
Expected: "/a",
147+
},
148+
{
149+
Case: "only current_dir",
150+
Data: &segments.AntigravityData{Workspace: segments.AntigravityWorkspace{CurrentDir: "/a"}},
151+
Expected: "/a",
152+
},
153+
{
154+
Case: "only cwd",
155+
Data: &segments.AntigravityData{CWD: "/b"},
156+
Expected: "/b",
157+
},
158+
{
159+
Case: "neither",
160+
Data: &segments.AntigravityData{},
161+
Expected: "",
162+
},
163+
{
164+
Case: "project_dir is not a fallback",
165+
Data: &segments.AntigravityData{Workspace: segments.AntigravityWorkspace{ProjectDir: "/p"}},
166+
Expected: "",
167+
},
168+
}
169+
170+
for _, tc := range cases {
171+
assert.Equal(t, tc.Expected, antigravityPWD(tc.Data), tc.Case)
172+
}
173+
}
174+
137175
func TestRunStatuslineRendersToWriter(t *testing.T) {
138176
t.Setenv("OMP_CACHE_DIR", t.TempDir())
139177
t.Setenv("POSH_SESSION_ID", "d4-writer")

src/config/default.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ func CopilotCLI() *Config {
235235
return statuslineCLIConfig(1234567891, COPILOTCLI, " \uec1e {{ .Model.DisplayName }} \uf2d0 {{ .TokenGauge }} ")
236236
}
237237

238+
func Antigravity() *Config {
239+
return statuslineCLIConfig(1234567892, ANTIGRAVITY, " \uf135 {{ .Model.DisplayName }} \uf2d0 {{ .TokenGauge }} ")
240+
}
241+
238242
// The left block is always PATH + GIT; the right block contains a single
239243
// segment of the given type and template.
240244
func statuslineCLIConfig(hash uint64, segmentType SegmentType, template string) *Config {

src/config/segment_registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func newSegmentWriter(segmentType SegmentType) (SegmentWriter, error) {
2525
func init() {
2626
gob.Register(&segments.Angular{})
2727
gob.Register(&segments.Version{})
28+
gob.Register(&segments.Antigravity{})
29+
gob.Register(&segments.AntigravityData{})
2830
gob.Register(&segments.Argocd{})
2931
gob.Register(&segments.Aspire{})
3032
gob.Register(&segments.Aurelia{})
@@ -144,6 +146,7 @@ func init() {
144146

145147
var Segments = map[SegmentType]func() SegmentWriter{
146148
ANGULAR: func() SegmentWriter { return &segments.Angular{} },
149+
ANTIGRAVITY: func() SegmentWriter { return &segments.Antigravity{} },
147150
ARGOCD: func() SegmentWriter { return &segments.Argocd{} },
148151
ASPIRE: func() SegmentWriter { return &segments.Aspire{} },
149152
AURELIA: func() SegmentWriter { return &segments.Aurelia{} },

src/config/segment_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const (
3030
Diamond SegmentStyle = "diamond"
3131
// ANGULAR writes which angular cli version us currently active
3232
ANGULAR SegmentType = "angular"
33+
// ANTIGRAVITY writes Antigravity CLI session information
34+
ANTIGRAVITY SegmentType = "antigravity"
3335
// ARGOCD writes the current argocd context
3436
ARGOCD SegmentType = "argocd"
3537
// ASPIRE writes the Aspire apphost status

src/segments/antigravity.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package segments
2+
3+
import (
4+
"github.com/jandedobbeleer/oh-my-posh/src/cache"
5+
"github.com/jandedobbeleer/oh-my-posh/src/log"
6+
"github.com/jandedobbeleer/oh-my-posh/src/text"
7+
)
8+
9+
type Antigravity struct {
10+
Base
11+
markedChar string
12+
unmarkedChar string
13+
AntigravityData
14+
}
15+
16+
type AntigravityData struct {
17+
Quota map[string]AntigravityQuota `json:"quota"`
18+
VCS *AntigravityVCS `json:"vcs"`
19+
Sandbox *AntigravitySandbox `json:"sandbox"`
20+
Vim *ClaudeVim `json:"vim"`
21+
Model AIModel `json:"model"`
22+
Workspace AntigravityWorkspace `json:"workspace"`
23+
TranscriptPath string `json:"transcript_path"`
24+
CWD string `json:"cwd"`
25+
SessionID string `json:"session_id"`
26+
ConversationID string `json:"conversation_id"`
27+
Version string `json:"version"`
28+
Product string `json:"product"`
29+
AgentState string `json:"agent_state"`
30+
PlanTier string `json:"plan_tier"`
31+
Email string `json:"email"`
32+
ExecutionMode string `json:"execution_mode"`
33+
ContextWindow AntigravityContextWindow `json:"context_window"`
34+
ArtifactCount int `json:"artifact_count"`
35+
PendingInputCount int `json:"pending_input_count"`
36+
TaskCount int `json:"task_count"`
37+
TerminalWidth int `json:"terminal_width"`
38+
Exceeds200KTokens bool `json:"exceeds_200k_tokens"`
39+
ToolConfirmationPending bool `json:"tool_confirmation_pending"`
40+
}
41+
42+
type AntigravityWorkspace struct {
43+
CurrentDir string `json:"current_dir"`
44+
ProjectDir string `json:"project_dir"`
45+
}
46+
47+
type AntigravityContextWindow struct {
48+
CurrentUsage *AntigravityCurrentUsage `json:"current_usage"`
49+
UsedPercentage *float64 `json:"used_percentage"`
50+
RemainingPercentage *float64 `json:"remaining_percentage"`
51+
TotalInputTokens int `json:"total_input_tokens"`
52+
TotalOutputTokens int `json:"total_output_tokens"`
53+
ContextWindowSize int `json:"context_window_size"`
54+
}
55+
56+
// Reflects the last API call, not a cumulative total.
57+
type AntigravityCurrentUsage struct {
58+
InputTokens int `json:"input_tokens"`
59+
OutputTokens int `json:"output_tokens"`
60+
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
61+
CacheReadInputTokens int `json:"cache_read_input_tokens"`
62+
}
63+
64+
// Keyed by a model or quota bucket id (e.g. "gemini-weekly"), not necessarily a model id.
65+
type AntigravityQuota struct {
66+
RemainingFraction *float64 `json:"remaining_fraction"`
67+
ResetInSeconds *int `json:"reset_in_seconds"`
68+
ResetTime string `json:"reset_time"`
69+
}
70+
71+
// Nil when the session is not backed by a VCS checkout.
72+
type AntigravityVCS struct {
73+
Type string `json:"type"`
74+
Branch string `json:"branch"`
75+
Dirty bool `json:"dirty"`
76+
}
77+
78+
// Nil when sandboxing is not in use.
79+
type AntigravitySandbox struct {
80+
AllowNetwork *bool `json:"allow_network"`
81+
Enabled bool `json:"enabled"`
82+
}
83+
84+
func (a *Antigravity) Template() string {
85+
return " \uf135 {{ .Model.DisplayName }} \uf2d0 {{ .TokenGauge }} "
86+
}
87+
88+
func (a *Antigravity) Enabled() bool {
89+
log.Debug("antigravity segment: checking if enabled")
90+
91+
data, found := cache.Get[AntigravityData](cache.Session, cache.ANTIGRAVITYCACHE)
92+
if !found {
93+
log.Debug("antigravity segment: no data found in session cache")
94+
return false
95+
}
96+
97+
log.Debug("antigravity segment: found data in session cache")
98+
log.Debugf("antigravity segment: model=%s, session=%s", data.Model.DisplayName, data.SessionID)
99+
100+
a.AntigravityData = data
101+
102+
a.markedChar = a.options.String(gaugeMarkedChar, "▰")
103+
a.unmarkedChar = a.options.String(gaugeUnmarkedChar, "▱")
104+
105+
return true
106+
}
107+
108+
// Uses pre-calculated UsedPercentage when available, falls back to calculating from
109+
// CurrentUsage, then to total tokens for backwards compatibility.
110+
func (a *Antigravity) TokenUsagePercent() text.Percentage {
111+
if a.ContextWindow.UsedPercentage != nil {
112+
v := *a.ContextWindow.UsedPercentage
113+
if v > 100 {
114+
return 100
115+
}
116+
117+
if v < 0 {
118+
return 0
119+
}
120+
121+
return text.Percentage(int(v + 0.5))
122+
}
123+
124+
if a.ContextWindow.ContextWindowSize <= 0 {
125+
return 0
126+
}
127+
128+
var currentTokens int
129+
if a.ContextWindow.CurrentUsage != nil {
130+
currentTokens = a.ContextWindow.CurrentUsage.InputTokens +
131+
a.ContextWindow.CurrentUsage.CacheCreationInputTokens +
132+
a.ContextWindow.CurrentUsage.CacheReadInputTokens
133+
}
134+
135+
if currentTokens <= 0 {
136+
currentTokens = a.ContextWindow.TotalInputTokens + a.ContextWindow.TotalOutputTokens
137+
}
138+
139+
if currentTokens <= 0 {
140+
return 0
141+
}
142+
143+
percent := (float64(currentTokens) * 100.0) / float64(a.ContextWindow.ContextWindowSize)
144+
145+
rounded := int(percent + 0.5)
146+
if rounded > 100 {
147+
return 100
148+
}
149+
150+
return text.Percentage(rounded)
151+
}
152+
153+
// Shows remaining capacity; see TokenGaugeUsed for the used view.
154+
func (a *Antigravity) TokenGauge() string {
155+
return a.TokenUsagePercent().GaugeWith(a.markedChar, a.unmarkedChar)
156+
}
157+
158+
// Shows used capacity, unlike TokenGauge which shows remaining.
159+
func (a *Antigravity) TokenGaugeUsed() string {
160+
return a.TokenUsagePercent().GaugeUsedWith(a.markedChar, a.unmarkedChar)
161+
}
162+
163+
// Uses CurrentUsage (actual context, resets on compact/clear), falling back to total tokens.
164+
func (a *Antigravity) FormattedTokens() string {
165+
var currentTokens int
166+
167+
if a.ContextWindow.CurrentUsage != nil {
168+
currentTokens = a.ContextWindow.CurrentUsage.InputTokens +
169+
a.ContextWindow.CurrentUsage.CacheCreationInputTokens +
170+
a.ContextWindow.CurrentUsage.CacheReadInputTokens
171+
}
172+
173+
if currentTokens <= 0 {
174+
currentTokens = a.ContextWindow.TotalInputTokens + a.ContextWindow.TotalOutputTokens
175+
}
176+
177+
return formatTokenCount(currentTokens)
178+
}
179+
180+
func (a *Antigravity) RemainingPercent() text.Percentage {
181+
if a.ContextWindow.RemainingPercentage != nil {
182+
v := *a.ContextWindow.RemainingPercentage
183+
if v > 100 {
184+
return 100
185+
}
186+
187+
if v < 0 {
188+
return 0
189+
}
190+
191+
return text.Percentage(int(v + 0.5))
192+
}
193+
194+
remaining := 100 - int(a.TokenUsagePercent())
195+
if remaining < 0 {
196+
return 0
197+
}
198+
199+
return text.Percentage(remaining)
200+
}

0 commit comments

Comments
 (0)