@narumitw/pi-statusline 0.20.0 → 0.23.0
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 +143 -95
- package/package.json +3 -4
- package/src/commands.ts +118 -0
- package/src/extension-status.ts +29 -32
- package/src/git-status.ts +11 -3
- package/src/render.ts +104 -64
- package/src/settings.ts +492 -109
- package/src/statusline.ts +51 -19
- package/src/tokyo-night.ts +180 -0
- package/src/types.ts +67 -0
- package/presets/classic.ts +0 -55
- package/presets/tokyo-night.ts +0 -84
- package/presets/types.ts +0 -38
- /package/{presets → src}/ansi.ts +0 -0
package/README.md
CHANGED
|
@@ -1,159 +1,207 @@
|
|
|
1
|
-
# ✨ pi-statusline —
|
|
1
|
+
# ✨ pi-statusline — Configurable Tokyo Night Footer for Pi
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-statusline) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@narumitw/pi-statusline` is a native [Pi coding agent](https://pi.dev) extension that replaces Pi's footer with a
|
|
5
|
+
`@narumitw/pi-statusline` is a native [Pi coding agent](https://pi.dev) extension that replaces Pi's footer with a configurable Tokyo Night powerline statusline.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Shows provider, model, thinking, directory, Git/PR state, tools, context, tokens, cost, time, and extension statuses.
|
|
10
|
+
- Uses one Starship-inspired `░▒▓` / `` Tokyo Night layout.
|
|
11
|
+
- Configures segment order, visibility, multiline breaks, surrounding text, palette, density, separators, and extension icons through JSON.
|
|
12
|
+
- Creates a complete editable default configuration on first session start.
|
|
13
|
+
- Applies validated `/statusline settings` edits immediately after an atomic save.
|
|
14
|
+
- Caches Git state outside footer rendering and guards stale session results.
|
|
15
|
+
- Wraps extension statuses safely at narrow terminal widths.
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
- Shows model, thinking level, git branch/status, project directory, active tool, context usage, tokens, cost, and clock.
|
|
13
|
-
- Displays compact statuses published through Pi's generic extension status API.
|
|
14
|
-
- Owns extension status icons through optional JSON config, including per-extension icon suppression with `""`.
|
|
15
|
-
- Warns when the same extension package is installed from multiple sources.
|
|
16
|
-
- Uses emoji-labeled segments for readability in both classic and Tokyo Night presets.
|
|
17
|
-
- Adapts to terminal width and wraps long extension status lines safely.
|
|
18
|
-
- Requires no configuration, with optional preset selection through `PI_STATUSLINE_PRESET`.
|
|
19
|
-
|
|
20
|
-
## 📦 Install
|
|
17
|
+
## Install
|
|
21
18
|
|
|
22
19
|
```bash
|
|
23
20
|
pi install npm:@narumitw/pi-statusline
|
|
24
21
|
```
|
|
25
22
|
|
|
26
|
-
Try
|
|
23
|
+
Try it once or from a checkout:
|
|
27
24
|
|
|
28
25
|
```bash
|
|
29
26
|
pi -e npm:@narumitw/pi-statusline
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Try this package locally from the repository root:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
27
|
pi -e ./extensions/pi-statusline
|
|
36
28
|
```
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
Do not enable this together with `@narumitw/pi-starship`: both extensions own Pi's footer. Use `pi-starship` instead when you need full Starship-style format/style grammar.
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
## Configuration
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
PI_STATUSLINE_PRESET=tokyo-night pi
|
|
44
|
-
PI_STATUSLINE_PRESET=classic pi
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Supported presets:
|
|
34
|
+
The only configuration source is:
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
-
|
|
36
|
+
```text
|
|
37
|
+
<getAgentDir()>/pi-statusline.json
|
|
38
|
+
```
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
On first session start, the extension atomically creates the complete default document. It never overwrites an existing malformed or unreadable file. A valid legacy `pi-statusline-settings.json` is migrated by preserving its original bytes; the canonical filename wins when both exist.
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
There are no project overrides or environment-variable overrides.
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
### Default JSON
|
|
57
45
|
|
|
58
46
|
```json
|
|
59
47
|
{
|
|
48
|
+
"palette": "tokyo-night",
|
|
49
|
+
"density": "compact",
|
|
50
|
+
"separator": "none",
|
|
51
|
+
"segments": [
|
|
52
|
+
"brand",
|
|
53
|
+
"provider",
|
|
54
|
+
"model",
|
|
55
|
+
"thinking",
|
|
56
|
+
"cwd",
|
|
57
|
+
"branch",
|
|
58
|
+
"tools",
|
|
59
|
+
"context",
|
|
60
|
+
"tokens",
|
|
61
|
+
"cost",
|
|
62
|
+
"time"
|
|
63
|
+
],
|
|
64
|
+
"segmentText": {
|
|
65
|
+
"brand": { "prefix": "", "suffix": "" },
|
|
66
|
+
"provider": { "prefix": "🔌 ", "suffix": "" },
|
|
67
|
+
"model": { "prefix": "🤖 ", "suffix": "" },
|
|
68
|
+
"thinking": { "prefix": "🧠 ", "suffix": "" },
|
|
69
|
+
"cwd": { "prefix": "📁 ", "suffix": "" },
|
|
70
|
+
"branch": { "prefix": "🌿 ", "suffix": "" },
|
|
71
|
+
"tools": { "prefix": "", "suffix": "" },
|
|
72
|
+
"context": { "prefix": "🪟 ctx ", "suffix": "" },
|
|
73
|
+
"tokens": { "prefix": "🔢 ", "suffix": "" },
|
|
74
|
+
"cost": { "prefix": "💸 $", "suffix": "" },
|
|
75
|
+
"time": { "prefix": "🕒 ", "suffix": "" },
|
|
76
|
+
"turn": { "prefix": "🔁 #", "suffix": "" }
|
|
77
|
+
},
|
|
60
78
|
"extensionStatusIcons": {
|
|
61
|
-
"
|
|
79
|
+
"chrome-devtools": "🌐",
|
|
80
|
+
"codex-usage": "📊",
|
|
81
|
+
"caffeinate": "💊",
|
|
82
|
+
"firecrawl": "🔥",
|
|
62
83
|
"github-pr": "🔎",
|
|
63
84
|
"goal": "🎯",
|
|
64
|
-
"
|
|
65
|
-
"unknown-error-retry": "",
|
|
85
|
+
"lsp": "🧰",
|
|
66
86
|
"plan-mode": "📝",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
87
|
+
"pisync": "🔄",
|
|
88
|
+
"subagents": "🧑🤝🧑",
|
|
89
|
+
"unknown-error-retry": "🔁"
|
|
69
90
|
}
|
|
70
91
|
}
|
|
71
92
|
```
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
All fields are optional in an existing document. Missing fields use defaults.
|
|
95
|
+
|
|
96
|
+
### Appearance
|
|
97
|
+
|
|
98
|
+
- `palette`: `tokyo-night`, `ocean`, `sunset`, `forest`, `candy`, `neon`, or `mono`.
|
|
99
|
+
- `density`: `compact` or `cozy`.
|
|
100
|
+
- `separator`: `none`, `dot`, `bar`, `powerline`, or `round`.
|
|
101
|
+
|
|
102
|
+
The separator applies only between adjacent segments in the same color block. Color-block transitions always use ``. Extension statuses remain on separate wrapped lines with their own palette-colored separator.
|
|
103
|
+
|
|
104
|
+
### Segments
|
|
105
|
+
|
|
106
|
+
`segments` is an ordered list containing:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
brand provider model thinking cwd branch tools context tokens cost time turn line_break
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The array controls visibility and actual rendering order. Data segments must remain unique. The special `line_break` segment starts another footer row and may repeat when another segment separates each occurrence; consecutive `line_break` entries are invalid. Each row receives its own powerline start and end. `line_break` has no `segmentText` entry.
|
|
113
|
+
|
|
114
|
+
An empty array hides the main powerline while still allowing extension statuses to render. `turn` is available but omitted by default.
|
|
115
|
+
|
|
116
|
+
Example multiline layout:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"segments": ["model", "line_break", "cwd", "line_break", "branch"]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This is valid because the `line_break` entries are separated. `["model", "line_break", "line_break", "cwd"]` is invalid.
|
|
125
|
+
|
|
126
|
+
### Segment text
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
- Installed extension id: for installed packages, use the package name/source such as `"@vendor/pi-foo"`, `"npm:@vendor/pi-foo@1.2.3"`, `"pi-foo"`, or the derived key `"foo"`.
|
|
77
|
-
- Namespaced status keys: package `@vendor/pi-foo` can match `foo`, `foo:server`, and `foo/server`, but not fuzzy matches like `foobar`.
|
|
78
|
-
- Missing key: use the built-in icon, or `🔌` for an unknown status key.
|
|
79
|
-
- String value: use that string as the icon.
|
|
80
|
-
- Empty string: show the status text without an icon.
|
|
81
|
-
- If multiple installed packages derive the same key, use the exact status key to disambiguate.
|
|
82
|
-
- `PI_STATUSLINE_PRESET` remains the only preset setting; this JSON file only controls extension status icons.
|
|
128
|
+
Each visible segment renders as:
|
|
83
129
|
|
|
84
|
-
|
|
130
|
+
```text
|
|
131
|
+
prefix + Pi-owned dynamic value + suffix
|
|
132
|
+
```
|
|
85
133
|
|
|
86
|
-
|
|
134
|
+
Override either string independently:
|
|
87
135
|
|
|
88
|
-
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"segmentText": {
|
|
139
|
+
"provider": { "prefix": "Provider: " },
|
|
140
|
+
"context": { "prefix": "[", "suffix": "]" },
|
|
141
|
+
"cost": { "prefix": "Cost $", "suffix": " USD" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
89
145
|
|
|
90
|
-
-
|
|
91
|
-
- 🤖 current model.
|
|
92
|
-
- 🧠 thinking level.
|
|
93
|
-
- 📁 current project directory.
|
|
94
|
-
- 🌿 git branch, with compact git status tokens when dirty or ahead/behind.
|
|
95
|
-
- ⚙ active or last tool.
|
|
96
|
-
- 🪟 context usage percentage.
|
|
97
|
-
- 🔢 token totals.
|
|
98
|
-
- 💸 estimated cost.
|
|
99
|
-
- 🕒 clock.
|
|
146
|
+
Prefix and suffix values must be single-line text without terminal control characters; use the `line_break` segment for additional rows.
|
|
100
147
|
|
|
101
|
-
|
|
148
|
+
This structured model intentionally does not provide variables or a format language. Dynamic Git, PR, activity, usage, token, and cost formatting remains owned by the extension.
|
|
102
149
|
|
|
103
|
-
|
|
150
|
+
### Extension status icons
|
|
104
151
|
|
|
105
|
-
`
|
|
152
|
+
`extensionStatusIcons` preserves these rules:
|
|
106
153
|
|
|
107
|
-
|
|
154
|
+
- An exact status key wins, such as `goal` or `foo:server`.
|
|
155
|
+
- Installed package aliases such as `@vendor/pi-foo`, `npm:@vendor/pi-foo@1.2.3`, `pi-foo`, or `foo` can configure namespaced statuses.
|
|
156
|
+
- An empty string hides the icon but keeps the status text.
|
|
157
|
+
- A missing key uses the built-in icon or `🔌` for an unknown key.
|
|
158
|
+
- Ambiguous package aliases require an exact status key.
|
|
108
159
|
|
|
109
|
-
|
|
110
|
-
- `🔎 PR #123 checks passing` for `github-pr: PR #123 checks passing` using the built-in `github-pr` icon.
|
|
111
|
-
- `☕ display` when JSON config sets `"caffeinate": "☕"`.
|
|
112
|
-
- `🧪 running` for third-party status `foo:server` when an installed package is named `@vendor/pi-foo` and JSON config sets `"@vendor/pi-foo": "🧪"`.
|
|
113
|
-
- `receiving` when JSON config sets `"unknown-error-retry": ""`.
|
|
114
|
-
- `🔌 running` for an unknown extension status key with no configured icon.
|
|
115
|
-
- `⚠️ dup biome-lsp` when local and npm installs register the same extension.
|
|
160
|
+
Statuses from other extensions appear below the main powerline. The linked GitHub PR status is hidden from that line when the branch segment already renders it.
|
|
116
161
|
|
|
117
|
-
##
|
|
162
|
+
## Commands
|
|
118
163
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
| Command | Purpose |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `/statusline settings` | Edit raw JSON in TUI, validate, atomically save, and apply immediately |
|
|
167
|
+
| `/statusline status` | Show settings path/source, effective appearance, segments, and diagnostics |
|
|
168
|
+
| `/statusline help` | Show command and schema guidance |
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
Invalid or cancelled edits leave both the previous file and effective runtime configuration unchanged. The editor is TUI-only; status and help are safe in TUI, print, JSON, and RPC modes.
|
|
126
171
|
|
|
127
|
-
|
|
172
|
+
## Git and activity details
|
|
173
|
+
|
|
174
|
+
Git status tokens are hidden for clean repositories. When present, they mean `⇡` ahead, `⇣` behind, `+` staged, `~` modified/deleted, `?` untracked, and `!` conflicts.
|
|
175
|
+
|
|
176
|
+
The tools segment distinguishes active tools, streaming/thinking, the last completed tool, and idle state. Parallel calls are summarized without running subprocesses during footer rendering.
|
|
177
|
+
|
|
178
|
+
## Package layout
|
|
179
|
+
|
|
180
|
+
```text
|
|
128
181
|
extensions/pi-statusline/
|
|
129
182
|
├── src/
|
|
130
|
-
│ ├── statusline.ts # Pi entrypoint and watcher lifecycle
|
|
131
|
-
│ └── *.ts # Package-local git, extension status, settings, and render modules
|
|
132
|
-
├── presets/
|
|
133
183
|
│ ├── ansi.ts
|
|
134
|
-
│ ├──
|
|
184
|
+
│ ├── commands.ts
|
|
185
|
+
│ ├── extension-status.ts
|
|
186
|
+
│ ├── git-status.ts
|
|
187
|
+
│ ├── render.ts
|
|
188
|
+
│ ├── settings.ts
|
|
189
|
+
│ ├── statusline.ts
|
|
135
190
|
│ ├── tokyo-night.ts
|
|
136
191
|
│ └── types.ts
|
|
192
|
+
├── test/
|
|
137
193
|
├── README.md
|
|
138
194
|
├── LICENSE
|
|
139
195
|
├── tsconfig.json
|
|
140
196
|
└── package.json
|
|
141
197
|
```
|
|
142
198
|
|
|
143
|
-
Only `statusline.ts` is a Pi entrypoint
|
|
144
|
-
|
|
145
|
-
```json
|
|
146
|
-
{
|
|
147
|
-
"pi": {
|
|
148
|
-
"extensions": ["./src/statusline.ts"]
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
```
|
|
199
|
+
Only `src/statusline.ts` is a Pi entrypoint. Other modules are package-internal.
|
|
152
200
|
|
|
153
|
-
##
|
|
201
|
+
## Keywords
|
|
154
202
|
|
|
155
|
-
Pi extension, Pi coding agent, statusline,
|
|
203
|
+
Pi extension, Pi coding agent, configurable statusline, Tokyo Night, terminal footer, token usage, context window, model status, TypeScript Pi package.
|
|
156
204
|
|
|
157
|
-
##
|
|
205
|
+
## License
|
|
158
206
|
|
|
159
207
|
MIT. See [`LICENSE`](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-statusline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Pi extension that replaces the footer with an information-rich statusline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
|
-
"presets",
|
|
18
17
|
"README.md",
|
|
19
18
|
"LICENSE"
|
|
20
19
|
],
|
|
@@ -24,8 +23,8 @@
|
|
|
24
23
|
]
|
|
25
24
|
},
|
|
26
25
|
"scripts": {
|
|
27
|
-
"check": "biome check . && npm run typecheck",
|
|
28
|
-
"format": "biome check --write .",
|
|
26
|
+
"check": "biome check --vcs-use-ignore-file=false src test package.json tsconfig.json README.md && npm run typecheck",
|
|
27
|
+
"format": "biome check --write --vcs-use-ignore-file=false src test package.json tsconfig.json README.md",
|
|
29
28
|
"typecheck": "tsc --noEmit"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_STATUSLINE_DOCUMENT,
|
|
5
|
+
type LoadedStatuslineSettings,
|
|
6
|
+
saveStatuslineSettingsDocument,
|
|
7
|
+
} from "./settings.js";
|
|
8
|
+
|
|
9
|
+
const SUBCOMMANDS: AutocompleteItem[] = [
|
|
10
|
+
{ value: "settings", label: "settings", description: "Edit pi-statusline.json" },
|
|
11
|
+
{ value: "status", label: "status", description: "Show effective statusline settings" },
|
|
12
|
+
{ value: "help", label: "help", description: "Show configuration help" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface StatuslineCommandOptions {
|
|
16
|
+
settingsPath: string;
|
|
17
|
+
getLoaded(): LoadedStatuslineSettings;
|
|
18
|
+
apply(loaded: LoadedStatuslineSettings, ctx: ExtensionCommandContext): void;
|
|
19
|
+
save?: (settingsPath: string, rawDocument: string) => LoadedStatuslineSettings;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerStatuslineCommand(pi: ExtensionAPI, options: StatuslineCommandOptions) {
|
|
23
|
+
pi.registerCommand("statusline", {
|
|
24
|
+
description: "Edit or inspect the Tokyo Night footer settings",
|
|
25
|
+
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
26
|
+
const normalized = prefix.trim().toLowerCase();
|
|
27
|
+
const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
|
|
28
|
+
return matches.length > 0 ? matches : null;
|
|
29
|
+
},
|
|
30
|
+
handler: async (args, ctx) => {
|
|
31
|
+
const subcommand = args.trim().split(/\s+/u)[0]?.toLowerCase() || "help";
|
|
32
|
+
switch (subcommand) {
|
|
33
|
+
case "settings":
|
|
34
|
+
await editSettings(ctx, options);
|
|
35
|
+
return;
|
|
36
|
+
case "status":
|
|
37
|
+
showStatus(ctx, options);
|
|
38
|
+
return;
|
|
39
|
+
case "help":
|
|
40
|
+
showHelp(ctx, options.settingsPath);
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
if (canNotify(ctx)) {
|
|
44
|
+
ctx.ui.notify(`Unknown /statusline subcommand: ${subcommand}`, "warning");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function editSettings(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
52
|
+
if (ctx.mode !== "tui") {
|
|
53
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${options.settingsPath}`, "info");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const current = options.getLoaded();
|
|
57
|
+
const edited = await ctx.ui.editor(
|
|
58
|
+
"pi-statusline.json — save and close to apply",
|
|
59
|
+
current.rawDocument ?? DEFAULT_STATUSLINE_DOCUMENT,
|
|
60
|
+
);
|
|
61
|
+
if (edited === undefined) return;
|
|
62
|
+
try {
|
|
63
|
+
const loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, edited);
|
|
64
|
+
options.apply(loaded, ctx);
|
|
65
|
+
const suffix =
|
|
66
|
+
loaded.diagnostics.length > 0
|
|
67
|
+
? ` (${loaded.diagnostics.length} warning${loaded.diagnostics.length === 1 ? "" : "s"})`
|
|
68
|
+
: "";
|
|
69
|
+
ctx.ui.notify(`pi-statusline settings saved and applied${suffix}.`, "info");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
ctx.ui.notify(`pi-statusline settings were not saved: ${formatError(error)}`, "error");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function showStatus(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
76
|
+
if (!canNotify(ctx)) return;
|
|
77
|
+
const loaded = options.getLoaded();
|
|
78
|
+
const diagnostics = loaded.diagnostics
|
|
79
|
+
.slice(0, 5)
|
|
80
|
+
.map((item) => `${item.path || "root"}: ${item.message}`)
|
|
81
|
+
.join("; ");
|
|
82
|
+
ctx.ui.notify(
|
|
83
|
+
[
|
|
84
|
+
`pi-statusline source: ${loaded.source}`,
|
|
85
|
+
`path: ${options.settingsPath}`,
|
|
86
|
+
`palette: ${loaded.config.palette}`,
|
|
87
|
+
`density: ${loaded.config.density}`,
|
|
88
|
+
`separator: ${loaded.config.separator}`,
|
|
89
|
+
`segments: ${loaded.config.segments.join(", ") || "none"}`,
|
|
90
|
+
diagnostics ? `warnings: ${diagnostics}` : "warnings: none",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
loaded.diagnostics.length > 0 ? "warning" : "info",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function showHelp(ctx: ExtensionCommandContext, settingsPath: string) {
|
|
97
|
+
if (!canNotify(ctx)) return;
|
|
98
|
+
ctx.ui.notify(
|
|
99
|
+
[
|
|
100
|
+
"/statusline settings — edit and apply JSON",
|
|
101
|
+
"/statusline status — show source, path, and warnings",
|
|
102
|
+
"/statusline help — show this help",
|
|
103
|
+
`Settings: ${settingsPath}`,
|
|
104
|
+
"Fields: palette, density, separator, segments, segmentText, extensionStatusIcons",
|
|
105
|
+
"Use line_break between segments for another footer row; repeats must not be consecutive.",
|
|
106
|
+
"The segmentText entries support prefix and suffix strings around Pi-owned dynamic values.",
|
|
107
|
+
].join("\n"),
|
|
108
|
+
"info",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function canNotify(ctx: ExtensionCommandContext): boolean {
|
|
113
|
+
return ctx.mode === "tui" || ctx.hasUI;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatError(error: unknown): string {
|
|
117
|
+
return error instanceof Error ? error.message : String(error);
|
|
118
|
+
}
|
package/src/extension-status.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
6
|
-
import {
|
|
7
|
-
import { tokyoNightExtensionSeparator } from "
|
|
8
|
-
import type { StatuslineConfig
|
|
6
|
+
import { DEFAULT_EXTENSION_STATUS_ICONS } from "./settings.js";
|
|
7
|
+
import { tokyoNightExtensionSeparator } from "./tokyo-night.js";
|
|
8
|
+
import type { StatuslineConfig } from "./types.js";
|
|
9
9
|
|
|
10
10
|
export type ExtensionStatusIconAliasMap = ReadonlyMap<string, readonly string[]>;
|
|
11
11
|
export interface ExtensionStatusRuntime {
|
|
@@ -14,26 +14,9 @@ export interface ExtensionStatusRuntime {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const STATUSLINE_KEY = "statusline";
|
|
17
|
-
const GITHUB_PR_KEY = "github-pr";
|
|
18
17
|
const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"codex-usage": "📊",
|
|
22
|
-
caffeinate: "💊",
|
|
23
|
-
firecrawl: "🔥",
|
|
24
|
-
"github-pr": "🔎",
|
|
25
|
-
goal: "🎯",
|
|
26
|
-
lsp: "🧰",
|
|
27
|
-
"plan-mode": "📝",
|
|
28
|
-
pisync: "🔄",
|
|
29
|
-
subagents: "🧑🤝🧑",
|
|
30
|
-
"unknown-error-retry": "🔁",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function extensionStatusSeparator(presetName: StatuslinePresetName, theme: Theme): string {
|
|
34
|
-
return presetName === "classic"
|
|
35
|
-
? classicExtensionSeparator(theme)
|
|
36
|
-
: tokyoNightExtensionSeparator(theme);
|
|
18
|
+
function extensionStatusSeparator(config: StatuslineConfig, theme: Theme): string {
|
|
19
|
+
return tokyoNightExtensionSeparator(theme, config.palette);
|
|
37
20
|
}
|
|
38
21
|
|
|
39
22
|
export function formatExtensionStatuses(
|
|
@@ -41,15 +24,14 @@ export function formatExtensionStatuses(
|
|
|
41
24
|
theme: Theme,
|
|
42
25
|
config: StatuslineConfig,
|
|
43
26
|
runtime: ExtensionStatusRuntime,
|
|
27
|
+
hiddenKeys: ReadonlySet<string> = new Set(),
|
|
44
28
|
): string {
|
|
45
|
-
const separator = extensionStatusSeparator(config
|
|
29
|
+
const separator = extensionStatusSeparator(config, theme);
|
|
46
30
|
const visibleStatuses = [
|
|
47
31
|
...formatDuplicateExtensionStatus(runtime, theme),
|
|
48
32
|
...[...statuses.entries()]
|
|
49
|
-
// github-pr is rendered inline in the branch segment, so skip it here to avoid duplication.
|
|
50
33
|
.filter(
|
|
51
|
-
([key, value]) =>
|
|
52
|
-
key !== STATUSLINE_KEY && key !== GITHUB_PR_KEY && value.trim().length > 0,
|
|
34
|
+
([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0,
|
|
53
35
|
)
|
|
54
36
|
.map(([key, value]) =>
|
|
55
37
|
formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases),
|
|
@@ -70,7 +52,12 @@ export function formatExtensionStatus(
|
|
|
70
52
|
const text = simplifyExtensionStatusText(status.text);
|
|
71
53
|
const color = extensionColor(key, value);
|
|
72
54
|
const textColor = color === "warning" ? "warning" : "muted";
|
|
73
|
-
const icon = extensionStatusIcon(
|
|
55
|
+
const icon = extensionStatusIcon(
|
|
56
|
+
key,
|
|
57
|
+
status.icon,
|
|
58
|
+
config.extensionStatusIcons,
|
|
59
|
+
extensionStatusIconAliases,
|
|
60
|
+
);
|
|
74
61
|
const renderedText = theme.fg(textColor, text);
|
|
75
62
|
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
76
63
|
}
|
|
@@ -187,7 +174,9 @@ function extensionSettingsFiles(cwd: string): string[] {
|
|
|
187
174
|
].filter((file) => existsSync(file));
|
|
188
175
|
}
|
|
189
176
|
|
|
190
|
-
export function findDuplicateExtensions(
|
|
177
|
+
export function findDuplicateExtensions(
|
|
178
|
+
installedPackages: readonly InstalledExtensionPackage[],
|
|
179
|
+
): string[] {
|
|
191
180
|
const sourcesByPackage = new Map<string, Set<string>>();
|
|
192
181
|
|
|
193
182
|
for (const extensionPackage of installedPackages) {
|
|
@@ -207,17 +196,25 @@ export function buildExtensionStatusIconAliases(
|
|
|
207
196
|
const packageAliasesByStatusBase = new Map<string, Map<string, string[]>>();
|
|
208
197
|
|
|
209
198
|
for (const extensionPackage of installedPackages) {
|
|
210
|
-
const candidate = extensionStatusIconAliasCandidate(
|
|
199
|
+
const candidate = extensionStatusIconAliasCandidate(
|
|
200
|
+
extensionPackage.packageName,
|
|
201
|
+
extensionPackage.source,
|
|
202
|
+
);
|
|
211
203
|
if (!candidate) continue;
|
|
212
|
-
const aliasesByPackage =
|
|
204
|
+
const aliasesByPackage =
|
|
205
|
+
packageAliasesByStatusBase.get(candidate.statusBase) ?? new Map<string, string[]>();
|
|
213
206
|
const existingAliases = aliasesByPackage.get(extensionPackage.packageName) ?? [];
|
|
214
|
-
aliasesByPackage.set(
|
|
207
|
+
aliasesByPackage.set(
|
|
208
|
+
extensionPackage.packageName,
|
|
209
|
+
uniqueStrings([...existingAliases, ...candidate.aliases]),
|
|
210
|
+
);
|
|
215
211
|
packageAliasesByStatusBase.set(candidate.statusBase, aliasesByPackage);
|
|
216
212
|
}
|
|
217
213
|
|
|
218
214
|
const aliases = new Map<string, string[]>();
|
|
219
215
|
for (const [statusBase, aliasesByPackage] of packageAliasesByStatusBase) {
|
|
220
|
-
if (aliasesByPackage.size === 1)
|
|
216
|
+
if (aliasesByPackage.size === 1)
|
|
217
|
+
aliases.set(statusBase, [...aliasesByPackage.values()][0] ?? []);
|
|
221
218
|
}
|
|
222
219
|
return aliases;
|
|
223
220
|
}
|
package/src/git-status.ts
CHANGED
|
@@ -87,14 +87,22 @@ export function formatGitStatusSummary(summary: GitStatusSummary | undefined): s
|
|
|
87
87
|
.join(" ");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function
|
|
90
|
+
export function formatGitBranchValue(
|
|
91
91
|
branch: string | null,
|
|
92
92
|
status: GitStatusSummary | undefined,
|
|
93
93
|
pr?: string,
|
|
94
94
|
): string {
|
|
95
|
-
if (!branch) return "
|
|
95
|
+
if (!branch) return "no-git";
|
|
96
96
|
const suffixes = [formatGitStatusSummary(status), pr ? `(${pr})` : ""].filter(Boolean);
|
|
97
|
-
return suffixes.length > 0 ?
|
|
97
|
+
return suffixes.length > 0 ? `${branch} ${suffixes.join(" ")}` : branch;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatGitBranchText(
|
|
101
|
+
branch: string | null,
|
|
102
|
+
status: GitStatusSummary | undefined,
|
|
103
|
+
pr?: string,
|
|
104
|
+
): string {
|
|
105
|
+
return `🌿 ${formatGitBranchValue(branch, status, pr)}`;
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
export function gitStatusSummaryEqual(
|