pi-fancy-footer 0.3.0 → 0.3.2

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
@@ -4,11 +4,13 @@ A [pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
4
4
  extension that replaces the default footer with a compact, two-line fancy status
5
5
  footer.
6
6
 
7
- <!-- markdownlint-disable MD033 -->
8
- <p align="center">
9
- <img src="screenshot.png" alt="screenshot" />
10
- </p>
11
- <!-- markdownlint-enable MD033 -->
7
+ ![screenshot](screenshots/screenshot.png)
8
+
9
+ ## 📦 Install
10
+
11
+ ```bash
12
+ pi install npm:pi-fancy-footer
13
+ ```
12
14
 
13
15
  ## 📊 What it shows
14
16
 
@@ -19,13 +21,21 @@ footer.
19
21
  - Repo / path, branch, commit, and open PR number
20
22
  - Git diff stats and ahead/behind status
21
23
 
22
- ## 📦 Install
24
+ ## 📸 Screenshots
23
25
 
24
- Install as a pi package:
26
+ <!-- markdownlint-disable MD033 -->
25
27
 
26
- ```bash
27
- pi install npm:pi-fancy-footer
28
- ```
28
+ <img src="screenshots/light-blocks.png" alt="light theme – blocks style" width="600" /><br/>
29
+ <img src="screenshots/light-bars.png" alt="light theme – bars style" width="600" /><br/>
30
+ <img src="screenshots/light-circles.png" alt="light theme – circles style" width="600" /><br/>
31
+ <img src="screenshots/light-stars.png" alt="light theme – stars style" width="600" />
32
+
33
+ <img src="screenshots/dark-yellow.png" alt="dark theme – yellow accent" width="600" /><br/>
34
+ <img src="screenshots/dark-blue.png" alt="dark theme – blue accent" width="600" /><br/>
35
+ <img src="screenshots/dark-green.png" alt="dark theme – green accent" width="600" /><br/>
36
+ <img src="screenshots/dark-red.png" alt="dark theme – red accent" width="600" />
37
+
38
+ <!-- markdownlint-enable MD033 -->
29
39
 
30
40
  ## 🎮 Commands
31
41
 
@@ -124,21 +134,21 @@ leading widget icon.
124
134
 
125
135
  <!-- markdownlint-disable MD013 MD060 -->
126
136
 
127
- | Widget | nerd | emoji | unicode | ascii |
128
- | ------------------ | ------- | ---------- | ------- | -------- |
129
- | `model` | `󰧑` | `🤖` | `◉` | `%` |
130
- | `thinking` | `󰭻` | `🧠` | `✦` | `?` |
131
- | `context-capacity` | `` | `💾` | `□` | `[]` |
132
- | `context-bar` | `󰾆` | `🔋` | `◧` | `\|` |
133
- | `context-usage` | `` | `📈` | `■` | `~` |
134
- | `total-cost` | `$` | `💲` | `$` | `$` |
135
- | `location` | `` | `📁` | `⌂` | `/` |
136
- | `branch` | `` | `🌿` | `⎇` | `*` |
137
- | `commit` | `` | `🔖` | `#` | `#` |
138
- | `pull-request` | `` | `🔀` | `⇄` | `@` |
139
- | `diff-added` | `↗` | `➕` | `+` | `+` |
140
- | `diff-removed` | `↘` | `➖` | `−` | `-` |
141
- | `git-status` | `//` | `🔼/🔽/🔀` | `↑/↓/↕` | `^/_/<>` |
137
+ | Widget | nerd | emoji | unicode | ascii |
138
+ | ------------------ | ---- | ---------- | ------- | -------- |
139
+ | `model` | `󰧑` | `🤖` | `◉` | `%` |
140
+ | `thinking` | `󰭻` | `🧠` | `✦` | `?` |
141
+ | `context-capacity` | `` | `💾` | `□` | `[]` |
142
+ | `context-bar` | `󰾆` | `🔋` | `◧` | `\|` |
143
+ | `context-usage` | `` | `📈` | `■` | `~` |
144
+ | `total-cost` | `$` | `💲` | `$` | `$` |
145
+ | `location` | `` | `📁` | `⌂` | `/` |
146
+ | `branch` | `` | `🌿` | `⎇` | `*` |
147
+ | `commit` | `` | `🔖` | `#` | `#` |
148
+ | `pull-request` | `` | `🔀` | `⇄` | `@` |
149
+ | `diff-added` | `↗` | `➕` | `+` | `+` |
150
+ | `diff-removed` | `↘` | `➖` | `−` | `-` |
151
+ | `git-status` | `//` | `🔼/🔽/🔀` | `↑/↓/↕` | `^/_/<>` |
142
152
 
143
153
  <!-- markdownlint-enable MD013 MD060 -->
144
154
 
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
3
  import {
4
+ getAgentDir,
5
5
  getSettingsListTheme,
6
6
  type Theme,
7
7
  } from "@mariozechner/pi-coding-agent";
@@ -67,20 +67,6 @@ function readJsonObject(filePath: string): Record<string, unknown> | undefined {
67
67
  }
68
68
  }
69
69
 
70
- function expandHome(pathValue: string): string {
71
- if (pathValue === "~") return homedir();
72
- if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) {
73
- return join(homedir(), pathValue.slice(2));
74
- }
75
- return pathValue;
76
- }
77
-
78
- function getAgentDir(): string {
79
- return process.env.PI_CODING_AGENT_DIR
80
- ? expandHome(process.env.PI_CODING_AGENT_DIR)
81
- : join(homedir(), ".pi", "agent");
82
- }
83
-
84
70
  export function coerceCompactionSettings(
85
71
  value: unknown,
86
72
  fallback: CompactionSettingsSnapshot = DEFAULT_COMPACTION_SETTINGS,
@@ -800,11 +786,13 @@ export function genericFooterSettingsItems(
800
786
  label: "context bar style",
801
787
  currentValue: formatContextBarStyleValue(draft.contextBarStyle),
802
788
  values: CONTEXT_BAR_STYLE_IDS.map(formatContextBarStyleValue),
803
- description: "Choose the character style for the context usage bar: " +
789
+ description:
790
+ "Choose the character style for the context usage bar: " +
804
791
  CONTEXT_BAR_STYLES.map((s, i) => {
805
792
  const repr = s.label + " " + s.used + s.free + s.reserved;
806
793
  return i === CONTEXT_BAR_STYLES.length - 1 ? "or " + repr : repr;
807
- }).join(", ") + ".",
794
+ }).join(", ") +
795
+ ".",
808
796
  },
809
797
  {
810
798
  id: "defaultTextColor",
@@ -1,6 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { collectGitInfo, collectPullRequestInfo, shouldRefreshPullRequest } from "./git.ts";
3
+ import {
4
+ collectGitInfo,
5
+ collectPullRequestInfo,
6
+ shouldRefreshPullRequest,
7
+ } from "./git.ts";
4
8
 
5
9
  interface ExecInvocation {
6
10
  command: string;
@@ -15,30 +19,49 @@ interface ExecResult {
15
19
  stderr: string;
16
20
  }
17
21
 
18
- function createPi(execImpl: (call: ExecInvocation) => ExecResult | Promise<ExecResult>) {
22
+ function gitSubcommand(args: string[]): string {
23
+ return args[0] === "--no-optional-locks" ? (args[1] ?? "") : (args[0] ?? "");
24
+ }
25
+
26
+ function createPi(
27
+ execImpl: (call: ExecInvocation) => ExecResult | Promise<ExecResult>,
28
+ ) {
19
29
  const calls: ExecInvocation[] = [];
20
30
 
21
31
  return {
22
32
  calls,
23
33
  pi: {
24
- async exec(command: string, args: string[], options: { cwd: string; timeout?: number }) {
25
- const call = { command, args, cwd: options.cwd, timeout: options.timeout };
34
+ async exec(
35
+ command: string,
36
+ args: string[],
37
+ options: { cwd: string; timeout?: number },
38
+ ) {
39
+ const call = {
40
+ command,
41
+ args,
42
+ cwd: options.cwd,
43
+ timeout: options.timeout,
44
+ };
26
45
  calls.push(call);
27
46
  return await execImpl(call);
28
47
  },
29
48
  } as {
30
- exec(command: string, args: string[], options: { cwd: string; timeout?: number }): Promise<ExecResult>;
49
+ exec(
50
+ command: string,
51
+ args: string[],
52
+ options: { cwd: string; timeout?: number },
53
+ ): Promise<ExecResult>;
31
54
  },
32
55
  };
33
56
  }
34
57
 
35
58
  test("collectPullRequestInfo ignores foreign branch-name matches and falls back to gh pr view", async () => {
36
59
  const { pi, calls } = createPi(({ command, args }) => {
37
- if (command === "git" && args[0] === "rev-parse") {
60
+ if (command === "git" && gitSubcommand(args) === "rev-parse") {
38
61
  return { code: 0, stdout: "origin/fix-ci\n", stderr: "" };
39
62
  }
40
63
 
41
- if (command === "git" && args[0] === "config") {
64
+ if (command === "git" && gitSubcommand(args) === "config") {
42
65
  return {
43
66
  code: 0,
44
67
  stdout: [
@@ -110,18 +133,29 @@ test("collectPullRequestInfo ignores foreign branch-name matches and falls back
110
133
  assert.equal(result.pullRequestLookupEnabled, true);
111
134
  assert.notEqual(result.pullRequestLookupAt, 0);
112
135
  assert.equal(
113
- calls.some((call) => call.command === "gh" && call.args[0] === "pr" && call.args[1] === "view"),
136
+ calls.some(
137
+ (call) =>
138
+ call.command === "gh" &&
139
+ call.args[0] === "pr" &&
140
+ call.args[1] === "view",
141
+ ),
142
+ true,
143
+ );
144
+ assert.equal(
145
+ calls
146
+ .filter((call) => call.command === "git")
147
+ .every((call) => call.args[0] === "--no-optional-locks"),
114
148
  true,
115
149
  );
116
150
  });
117
151
 
118
152
  test("collectPullRequestInfo skips GitHub CLI lookups when the repository has no GitHub remote", async () => {
119
153
  const { pi, calls } = createPi(({ command, args }) => {
120
- if (command === "git" && args[0] === "rev-parse") {
154
+ if (command === "git" && gitSubcommand(args) === "rev-parse") {
121
155
  return { code: 0, stdout: "origin/main\n", stderr: "" };
122
156
  }
123
157
 
124
- if (command === "git" && args[0] === "config") {
158
+ if (command === "git" && gitSubcommand(args) === "config") {
125
159
  return {
126
160
  code: 0,
127
161
  stdout: "remote.origin.url ssh://git.example.com/team/repo.git",
@@ -136,12 +170,19 @@ test("collectPullRequestInfo skips GitHub CLI lookups when the repository has no
136
170
 
137
171
  assert.equal(result.pullRequest, undefined);
138
172
  assert.equal(result.pullRequestLookupEnabled, false);
139
- assert.equal(calls.every((call) => call.command === "git"), true);
173
+ assert.equal(
174
+ calls.every((call) => call.command === "git"),
175
+ true,
176
+ );
177
+ assert.equal(
178
+ calls.every((call) => call.args[0] === "--no-optional-locks"),
179
+ true,
180
+ );
140
181
  });
141
182
 
142
183
  test("collectGitInfo disables periodic PR refreshes for non-GitHub repositories", async () => {
143
- const { pi } = createPi(({ command, args }) => {
144
- if (command === "git" && args[0] === "status") {
184
+ const { pi, calls } = createPi(({ command, args }) => {
185
+ if (command === "git" && gitSubcommand(args) === "status") {
145
186
  return {
146
187
  code: 0,
147
188
  stdout: [
@@ -154,7 +195,7 @@ test("collectGitInfo disables periodic PR refreshes for non-GitHub repositories"
154
195
  };
155
196
  }
156
197
 
157
- if (command === "git" && args[0] === "config") {
198
+ if (command === "git" && gitSubcommand(args) === "config") {
158
199
  return {
159
200
  code: 0,
160
201
  stdout: "remote.origin.url ssh://git.example.com/team/repo.git",
@@ -162,7 +203,7 @@ test("collectGitInfo disables periodic PR refreshes for non-GitHub repositories"
162
203
  };
163
204
  }
164
205
 
165
- if (command === "git" && args[0] === "diff") {
206
+ if (command === "git" && gitSubcommand(args) === "diff") {
166
207
  return { code: 0, stdout: "", stderr: "" };
167
208
  }
168
209
 
@@ -173,4 +214,8 @@ test("collectGitInfo disables periodic PR refreshes for non-GitHub repositories"
173
214
 
174
215
  assert.equal(git.pullRequestLookupEnabled, false);
175
216
  assert.equal(shouldRefreshPullRequest(git), false);
217
+ assert.equal(
218
+ calls.every((call) => call.args[0] === "--no-optional-locks"),
219
+ true,
220
+ );
176
221
  });
@@ -5,7 +5,12 @@ import {
5
5
  selectPullRequestFromGraphQL,
6
6
  splitGitHubRepository,
7
7
  } from "./pull-request.ts";
8
- import { EMPTY_GIT_INFO, type GitInfo, parseNumstat, toNumber } from "./shared.ts";
8
+ import {
9
+ EMPTY_GIT_INFO,
10
+ type GitInfo,
11
+ parseNumstat,
12
+ toNumber,
13
+ } from "./shared.ts";
9
14
 
10
15
  interface ExecResult {
11
16
  code: number;
@@ -16,6 +21,7 @@ interface ExecResult {
16
21
  const DEFAULT_COMMAND_TIMEOUT_MS = 2_000;
17
22
  const GITHUB_COMMAND_TIMEOUT_MS = 5_000;
18
23
  const PULL_REQUEST_REFRESH_MS = 60_000;
24
+ const GIT_NO_OPTIONAL_LOCKS_ARG = "--no-optional-locks";
19
25
  const PULL_REQUEST_QUERY = [
20
26
  "query($owner: String!, $name: String!, $branch: String!) {",
21
27
  " repository(owner: $owner, name: $name) {",
@@ -61,6 +67,31 @@ async function exec(
61
67
  return result.stdout;
62
68
  }
63
69
 
70
+ async function execGitResult(
71
+ pi: ExtensionAPI,
72
+ args: string[],
73
+ cwd: string,
74
+ timeout = DEFAULT_COMMAND_TIMEOUT_MS,
75
+ ): Promise<ExecResult> {
76
+ return execResult(
77
+ pi,
78
+ "git",
79
+ [GIT_NO_OPTIONAL_LOCKS_ARG, ...args],
80
+ cwd,
81
+ timeout,
82
+ );
83
+ }
84
+
85
+ async function execGit(
86
+ pi: ExtensionAPI,
87
+ args: string[],
88
+ cwd: string,
89
+ ): Promise<string> {
90
+ const result = await execGitResult(pi, args, cwd);
91
+ if (result.code !== 0) return "";
92
+ return result.stdout;
93
+ }
94
+
64
95
  async function collectPullRequestFromBaseRepository(
65
96
  pi: ExtensionAPI,
66
97
  cwd: string,
@@ -111,16 +142,28 @@ async function collectCurrentBranchPullRequest(
111
142
  }
112
143
 
113
144
  export function shouldRefreshPullRequest(
114
- git: Pick<GitInfo, "branch" | "pullRequestLookupEnabled" | "pullRequestLookupAt">,
145
+ git: Pick<
146
+ GitInfo,
147
+ "branch" | "pullRequestLookupEnabled" | "pullRequestLookupAt"
148
+ >,
115
149
  ): boolean {
116
- return git.pullRequestLookupEnabled && !!git.branch && Date.now() - git.pullRequestLookupAt >= PULL_REQUEST_REFRESH_MS;
150
+ return (
151
+ git.pullRequestLookupEnabled &&
152
+ !!git.branch &&
153
+ Date.now() - git.pullRequestLookupAt >= PULL_REQUEST_REFRESH_MS
154
+ );
117
155
  }
118
156
 
119
157
  export async function collectPullRequestInfo(
120
158
  pi: ExtensionAPI,
121
159
  cwd: string,
122
160
  branch: string,
123
- ): Promise<Pick<GitInfo, "pullRequest" | "pullRequestLookupEnabled" | "pullRequestLookupAt">> {
161
+ ): Promise<
162
+ Pick<
163
+ GitInfo,
164
+ "pullRequest" | "pullRequestLookupEnabled" | "pullRequestLookupAt"
165
+ >
166
+ > {
124
167
  if (!branch) {
125
168
  return {
126
169
  pullRequest: undefined,
@@ -130,8 +173,12 @@ export async function collectPullRequestInfo(
130
173
  }
131
174
 
132
175
  const [upstream, remoteUrls] = await Promise.all([
133
- exec(pi, "git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], cwd),
134
- exec(pi, "git", ["config", "--get-regexp", "^remote\\..*\\.url$"], cwd),
176
+ execGit(
177
+ pi,
178
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
179
+ cwd,
180
+ ),
181
+ execGit(pi, ["config", "--get-regexp", "^remote\\..*\\.url$"], cwd),
135
182
  ]);
136
183
 
137
184
  const repositoryContext = createGitHubRepositoryContext(remoteUrls, upstream);
@@ -146,7 +193,13 @@ export async function collectPullRequestInfo(
146
193
  }
147
194
 
148
195
  for (const baseRepository of plan.baseRepositories) {
149
- const pullRequest = await collectPullRequestFromBaseRepository(pi, cwd, baseRepository, branch, plan.headOwners);
196
+ const pullRequest = await collectPullRequestFromBaseRepository(
197
+ pi,
198
+ cwd,
199
+ baseRepository,
200
+ branch,
201
+ plan.headOwners,
202
+ );
150
203
  if (pullRequest) {
151
204
  return {
152
205
  pullRequest,
@@ -169,11 +222,20 @@ export async function collectPullRequestInfo(
169
222
  export async function collectGitInfo(
170
223
  pi: ExtensionAPI,
171
224
  cwd: string,
172
- previousGit: Pick<GitInfo, "repository" | "branch" | "pullRequest" | "pullRequestLookupEnabled" | "pullRequestLookupAt"> | undefined = undefined,
225
+ previousGit:
226
+ | Pick<
227
+ GitInfo,
228
+ | "repository"
229
+ | "branch"
230
+ | "pullRequest"
231
+ | "pullRequestLookupEnabled"
232
+ | "pullRequestLookupAt"
233
+ >
234
+ | undefined = undefined,
173
235
  ): Promise<GitInfo> {
174
236
  const [porcelainV2, remoteUrls] = await Promise.all([
175
- exec(pi, "git", ["status", "--porcelain=2", "--branch"], cwd),
176
- exec(pi, "git", ["config", "--get-regexp", "^remote\\..*\\.url$"], cwd),
237
+ execGit(pi, ["status", "--porcelain=2", "--branch"], cwd),
238
+ execGit(pi, ["config", "--get-regexp", "^remote\\..*\\.url$"], cwd),
177
239
  ]);
178
240
 
179
241
  if (!porcelainV2) return { ...EMPTY_GIT_INFO };
@@ -221,7 +283,11 @@ export async function collectGitInfo(
221
283
  continue;
222
284
  }
223
285
 
224
- if (line.startsWith("1 ") || line.startsWith("2 ") || line.startsWith("u ")) {
286
+ if (
287
+ line.startsWith("1 ") ||
288
+ line.startsWith("2 ") ||
289
+ line.startsWith("u ")
290
+ ) {
225
291
  const xy = line.split(" ")[1] || "..";
226
292
  const x = xy[0] || ".";
227
293
  const y = xy[1] || ".";
@@ -231,22 +297,23 @@ export async function collectGitInfo(
231
297
  }
232
298
 
233
299
  const repositoryContext = createGitHubRepositoryContext(remoteUrls, upstream);
234
- const samePullRequestTarget = previousGit !== undefined
235
- && previousGit.repository === repositoryContext.repository
236
- && previousGit.branch === branch;
300
+ const samePullRequestTarget =
301
+ previousGit !== undefined &&
302
+ previousGit.repository === repositoryContext.repository &&
303
+ previousGit.branch === branch;
237
304
 
238
305
  let added = 0;
239
306
  let removed = 0;
240
307
 
241
- const headDiff = await exec(pi, "git", ["diff", "--numstat", "HEAD"], cwd);
308
+ const headDiff = await execGit(pi, ["diff", "--numstat", "HEAD"], cwd);
242
309
  if (headDiff) {
243
310
  const stats = parseNumstat(headDiff);
244
311
  added = stats.added;
245
312
  removed = stats.removed;
246
313
  } else {
247
314
  const [stagedDiff, unstagedDiff] = await Promise.all([
248
- exec(pi, "git", ["diff", "--numstat", "--cached"], cwd),
249
- exec(pi, "git", ["diff", "--numstat"], cwd),
315
+ execGit(pi, ["diff", "--numstat", "--cached"], cwd),
316
+ execGit(pi, ["diff", "--numstat"], cwd),
250
317
  ]);
251
318
  const stagedStats = parseNumstat(stagedDiff);
252
319
  const unstagedStats = parseNumstat(unstagedDiff);
@@ -260,7 +327,9 @@ export async function collectGitInfo(
260
327
  commit,
261
328
  pullRequest: samePullRequestTarget ? previousGit?.pullRequest : undefined,
262
329
  pullRequestLookupEnabled: repositoryContext.pullRequestLookupEnabled,
263
- pullRequestLookupAt: samePullRequestTarget ? previousGit?.pullRequestLookupAt ?? 0 : 0,
330
+ pullRequestLookupAt: samePullRequestTarget
331
+ ? (previousGit?.pullRequestLookupAt ?? 0)
332
+ : 0,
264
333
  added,
265
334
  removed,
266
335
  counts: {
@@ -4,7 +4,7 @@ import {
4
4
  } from "@mariozechner/pi-coding-agent";
5
5
  import {
6
6
  Key,
7
- getEditorKeybindings,
7
+ getKeybindings,
8
8
  matchesKey,
9
9
  truncateToWidth,
10
10
  visibleWidth,
@@ -543,19 +543,19 @@ export default function (pi: ExtensionAPI) {
543
543
  return;
544
544
  }
545
545
 
546
- const kb = getEditorKeybindings();
546
+ const kb = getKeybindings();
547
547
 
548
- if (kb.matches(data, "selectUp")) {
548
+ if (kb.matches(data, "tui.select.up")) {
549
549
  if (getActiveSectionItems().length > 0) moveSelection(-1);
550
- } else if (kb.matches(data, "selectDown")) {
550
+ } else if (kb.matches(data, "tui.select.down")) {
551
551
  if (getActiveSectionItems().length > 0) moveSelection(1);
552
552
  } else if (matchesKey(data, Key.tab)) {
553
553
  moveSection(1);
554
554
  } else if (matchesKey(data, Key.shift("tab"))) {
555
555
  moveSection(-1);
556
- } else if (kb.matches(data, "selectConfirm") || data === " ") {
556
+ } else if (kb.matches(data, "tui.select.confirm") || data === " ") {
557
557
  activateCurrentItem();
558
- } else if (kb.matches(data, "selectCancel")) {
558
+ } else if (kb.matches(data, "tui.select.cancel")) {
559
559
  done(undefined);
560
560
  return;
561
561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fancy-footer",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A fancy footer extension for pi",
5
5
  "type": "module",
6
6
  "files": [