pi-powerline-footer 0.2.21 → 0.2.23

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.23] - 2026-02-06
6
+
7
+ ### Fixed
8
+ - **Slash command autocomplete not appearing** — Custom editor created during `session_start` never received the autocomplete provider because pi v0.52.7 moved `setupAutocomplete()` to run after extensions load. The `handleInput` override now detects the missing provider on first keystroke, re-triggers `setEditorComponent` (which succeeds because the provider exists by then), and forwards the keystroke to the new editor. Users without editor-replacing extensions were unaffected.
9
+
10
+ ## [0.2.22] - 2026-01-31
11
+
12
+ ### Fixed
13
+ - **Detached HEAD flickering** — Git branch segment no longer oscillates between showing "detached" and hiding every 500ms when HEAD is detached
14
+ - Root cause: two competing branch detection methods (provider reads `.git/HEAD` → `"detached"`, extension runs `git branch --show-current` → empty/null) fought via a `??` fallback that leaked the provider value on every cache expiry
15
+ - Branch cache now returns stale value while refreshing instead of falling through to provider
16
+ - Detached HEAD now shows the short commit SHA (e.g., `abc1234 (detached)`) instead of bare "detached"
17
+
18
+ ### Changed
19
+ - **Extracted `runGit` helper** — Consolidated duplicated process-spawning logic from `fetchGitBranch` and `fetchGitStatus` into a shared helper
20
+ - `fetchGitBranch` now distinguishes "not a git repo" (null, early exit) from "detached HEAD" (empty string, SHA lookup) — avoids spawning a wasteful second process for non-git directories
21
+
5
22
  ## [0.2.21] - 2026-01-31
6
23
 
7
24
  ### Changed
package/git-status.ts CHANGED
@@ -60,12 +60,9 @@ function parseGitStatusOutput(output: string): { staged: number; unstaged: numbe
60
60
  return { staged, unstaged, untracked };
61
61
  }
62
62
 
63
- /**
64
- * Fetch current git branch asynchronously
65
- */
66
- async function fetchGitBranch(): Promise<string | null> {
63
+ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
67
64
  return new Promise((resolve) => {
68
- const proc = spawn("git", ["branch", "--show-current"], {
65
+ const proc = spawn("git", args, {
69
66
  stdio: ["ignore", "pipe", "pipe"],
70
67
  });
71
68
 
@@ -84,67 +81,40 @@ async function fetchGitBranch(): Promise<string | null> {
84
81
  });
85
82
 
86
83
  proc.on("close", (code) => {
87
- if (code !== 0) {
88
- finish(null);
89
- return;
90
- }
91
- const branch = stdout.trim();
92
- finish(branch || null); // Empty string means detached HEAD
84
+ finish(code === 0 ? stdout.trim() : null);
93
85
  });
94
86
 
95
87
  proc.on("error", () => {
96
88
  finish(null);
97
89
  });
98
90
 
99
- // Timeout after 200ms
100
91
  const timeoutId = setTimeout(() => {
101
92
  proc.kill();
102
93
  finish(null);
103
- }, 200);
94
+ }, timeoutMs);
104
95
  });
105
96
  }
106
97
 
107
98
  /**
108
- * Fetch git status asynchronously
99
+ * Fetch current git branch asynchronously.
100
+ * For detached HEAD, returns the short commit SHA (matches provider's "detached" behavior).
109
101
  */
110
- async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
111
- return new Promise((resolve) => {
112
- const proc = spawn("git", ["status", "--porcelain"], {
113
- stdio: ["ignore", "pipe", "pipe"],
114
- });
115
-
116
- let stdout = "";
117
- let resolved = false;
118
-
119
- const finish = (result: { staged: number; unstaged: number; untracked: number } | null) => {
120
- if (resolved) return;
121
- resolved = true;
122
- clearTimeout(timeoutId);
123
- resolve(result);
124
- };
125
-
126
- proc.stdout.on("data", (data) => {
127
- stdout += data.toString();
128
- });
129
-
130
- proc.on("close", (code) => {
131
- if (code !== 0) {
132
- finish(null);
133
- return;
134
- }
135
- finish(parseGitStatusOutput(stdout));
136
- });
102
+ async function fetchGitBranch(): Promise<string | null> {
103
+ const branch = await runGit(["branch", "--show-current"]);
104
+ if (branch === null) return null;
105
+ if (branch) return branch;
137
106
 
138
- proc.on("error", () => {
139
- finish(null);
140
- });
107
+ const sha = await runGit(["rev-parse", "--short", "HEAD"]);
108
+ return sha ? `${sha} (detached)` : "detached";
109
+ }
141
110
 
142
- // Timeout after 500ms
143
- const timeoutId = setTimeout(() => {
144
- proc.kill();
145
- finish(null);
146
- }, 500);
147
- });
111
+ /**
112
+ * Fetch git status asynchronously
113
+ */
114
+ async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
115
+ const output = await runGit(["status", "--porcelain"], 500);
116
+ if (output === null) return null;
117
+ return parseGitStatusOutput(output);
148
118
  }
149
119
 
150
120
  /**
@@ -174,8 +144,8 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
174
144
  });
175
145
  }
176
146
 
177
- // Return cached branch, or fall back to provider
178
- return cachedBranch?.branch ?? providerBranch;
147
+ // Return stale cache while refreshing; only use provider before first fetch
148
+ return cachedBranch ? cachedBranch.branch : providerBranch;
179
149
  }
180
150
 
181
151
  /**
package/index.ts CHANGED
@@ -550,14 +550,23 @@ export default function powerlineFooter(pi: ExtensionAPI) {
550
550
  function setupCustomEditor(ctx: any) {
551
551
  // Import CustomEditor dynamically and create wrapper
552
552
  import("@mariozechner/pi-coding-agent").then(({ CustomEditor }) => {
553
- ctx.ui.setEditorComponent((tui: any, editorTheme: any, keybindings: any) => {
553
+ let currentEditor: any = null;
554
+ let autocompleteFixed = false;
555
+
556
+ const editorFactory = (tui: any, editorTheme: any, keybindings: any) => {
554
557
  // Create custom editor that overrides render for status bar below content
555
558
  const editor = new CustomEditor(tui, editorTheme, keybindings);
559
+ currentEditor = editor;
556
560
 
557
- // Override handleInput to dismiss welcome on first keypress
558
561
  const originalHandleInput = editor.handleInput.bind(editor);
559
562
  editor.handleInput = (data: string) => {
560
- // Dismiss welcome overlay/header on first keypress (use setTimeout to avoid re-entrancy)
563
+ if (!autocompleteFixed && !(editor as any).autocompleteProvider) {
564
+ autocompleteFixed = true;
565
+ ctx.ui.setEditorComponent(editorFactory);
566
+ currentEditor?.handleInput(data);
567
+ return;
568
+ }
569
+ // Dismiss welcome overlay/header (use setTimeout to avoid re-entrancy)
561
570
  setTimeout(() => dismissWelcome(ctx), 0);
562
571
  originalHandleInput(data);
563
572
  };
@@ -632,7 +641,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
632
641
  };
633
642
 
634
643
  return editor;
635
- });
644
+ };
645
+
646
+ ctx.ui.setEditorComponent(editorFactory);
636
647
 
637
648
  // Set up footer data provider access (needed for git branch, extension statuses)
638
649
  // Status bar is rendered inside the editor override, footer is empty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-powerline-footer",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Powerline-style status bar extension for pi coding agent",
5
5
  "type": "module",
6
6
  "bin": {