pi-command-center 0.1.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/LICENSE +21 -0
- package/README.md +41 -0
- package/extensions/command-center/README.md +73 -0
- package/extensions/command-center/config.json.example +15 -0
- package/extensions/command-center/index.ts +443 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Warren Winter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Command Center for Pi (`pi-command-center`)
|
|
2
|
+
|
|
3
|
+
A scrollable overview of available /commands (from extensions, prompts, skills) shown as a widget above the editor. The editor stays fully interactive; you can keep the widget open while typing and submitting commands.
|
|
4
|
+
|
|
5
|
+
See source repo for more documentation.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
From npm:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pi install npm:pi-command-center
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From the dot314 git bundle (filtered install):
|
|
16
|
+
|
|
17
|
+
Add to `~/.pi/agent/settings.json` (or replace an existing unfiltered `git:github.com/w-winter/dot314` entry):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"packages": [
|
|
22
|
+
{
|
|
23
|
+
"source": "git:github.com/w-winter/dot314",
|
|
24
|
+
"extensions": ["extensions/command-center/index.ts"],
|
|
25
|
+
"skills": [],
|
|
26
|
+
"themes": [],
|
|
27
|
+
"prompts": []
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
This package ships a template at:
|
|
36
|
+
|
|
37
|
+
- `extensions/command-center/config.json.example`
|
|
38
|
+
|
|
39
|
+
Copy it to `config.json`, edit, then run `/reload`.
|
|
40
|
+
|
|
41
|
+
See `extensions/command-center/README.md` for the full config options.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Command Center
|
|
2
|
+
|
|
3
|
+
A scrollable overview of available /commands (from extensions, prompts, skills, and optionally† built-ins) shown as a widget above the editor. The editor stays fully interactive; you can keep the widget open while typing and submitting commands.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img width="333" alt="command center demo" src="https://github.com/user-attachments/assets/f9ed3649-ac5b-4658-836b-86091e4985a1" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
- Command: `/command-center` (toggle)
|
|
12
|
+
- Shortcut: configurable in `config.json`
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
1. Copy `config.json.example` → `config.json`
|
|
17
|
+
2. Edit `config.json` to your preferences
|
|
18
|
+
3. Run `/reload`
|
|
19
|
+
|
|
20
|
+
### Recommended defaults
|
|
21
|
+
|
|
22
|
+
#### Hide built-ins (†)
|
|
23
|
+
|
|
24
|
+
By default, this extension excludes built-in interactive commands because:
|
|
25
|
+
- Built-ins are already discoverable via the editor’s native `/` autocomplete (with descriptions)
|
|
26
|
+
- Keeping a built-in list inside this extension requires manual maintenance as pi evolves
|
|
27
|
+
|
|
28
|
+
If you still want them, set:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"display": { "includeBuiltins": true }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Widget height
|
|
37
|
+
|
|
38
|
+
You can force a fixed widget height (rows):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"layout": { "height": 14 }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Suggested values:
|
|
47
|
+
- Small terminals: **20–30** rows
|
|
48
|
+
- Larger terminals: increase as you like
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
- The widget height is clamped so the editor always has some space
|
|
52
|
+
- If pi can’t determine terminal height, Command Center assumes a fallback terminal height of **54** rows
|
|
53
|
+
(so the effective maximum widget height is typically **48** rows due to reserved editor space)
|
|
54
|
+
|
|
55
|
+
If `layout.height` is `null` or omitted, the widget auto-sizes based on terminal height.
|
|
56
|
+
|
|
57
|
+
### Keybindings
|
|
58
|
+
|
|
59
|
+
All shortcuts are configured here (strings are pi key ids):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"keybindings": {
|
|
64
|
+
"toggle": "ctrl+shift+/",
|
|
65
|
+
"scrollUp": "ctrl+shift+up",
|
|
66
|
+
"scrollDown": "ctrl+shift+down",
|
|
67
|
+
"scrollPageUp": null,
|
|
68
|
+
"scrollPageDown": null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Set any binding to `null` to disable it.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"display": {
|
|
3
|
+
"includeBuiltins": false
|
|
4
|
+
},
|
|
5
|
+
"layout": {
|
|
6
|
+
"height": 24
|
|
7
|
+
},
|
|
8
|
+
"keybindings": {
|
|
9
|
+
"toggle": "ctrl+shift+/",
|
|
10
|
+
"scrollUp": "ctrl+shift+up",
|
|
11
|
+
"scrollDown": "ctrl+shift+down",
|
|
12
|
+
"scrollPageUp": null,
|
|
13
|
+
"scrollPageDown": null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Center Extension
|
|
3
|
+
*
|
|
4
|
+
* A scrollable commands cheat sheet shown as a widget above the editor.
|
|
5
|
+
*
|
|
6
|
+
* Keybindings are configured in ./config.json (relative to this file).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext, SlashCommandInfo } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
// Note: pi.getCommands() does NOT include built-in interactive commands (e.g. /model, /settings)
|
|
17
|
+
// because those do not execute when sent via prompt. Until the extension API exposes built-ins,
|
|
18
|
+
// we keep a small local list in case includeBuiltins is configured true
|
|
19
|
+
const BUILTIN_COMMANDS: string[] = [
|
|
20
|
+
"/settings",
|
|
21
|
+
"/model",
|
|
22
|
+
"/scoped-models",
|
|
23
|
+
"/name",
|
|
24
|
+
"/session",
|
|
25
|
+
"/reload",
|
|
26
|
+
"/compact",
|
|
27
|
+
"/tree",
|
|
28
|
+
"/fork",
|
|
29
|
+
"/new",
|
|
30
|
+
"/resume",
|
|
31
|
+
"/export",
|
|
32
|
+
"/copy",
|
|
33
|
+
"/share",
|
|
34
|
+
"/hotkeys",
|
|
35
|
+
"/changelog",
|
|
36
|
+
"/login",
|
|
37
|
+
"/logout",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
type ExtensionKeybindingsConfig = {
|
|
41
|
+
toggle?: string | null;
|
|
42
|
+
scrollUp?: string | null;
|
|
43
|
+
scrollDown?: string | null;
|
|
44
|
+
scrollPageUp?: string | null;
|
|
45
|
+
scrollPageDown?: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type ExtensionLayoutConfig = {
|
|
49
|
+
/**
|
|
50
|
+
* Fixed widget height in rows.
|
|
51
|
+
*
|
|
52
|
+
* If omitted, height is computed from terminal height.
|
|
53
|
+
*/
|
|
54
|
+
height?: number | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type ExtensionDisplayConfig = {
|
|
58
|
+
/**
|
|
59
|
+
* Whether to include built-in interactive commands in the widget output
|
|
60
|
+
*
|
|
61
|
+
* Recommended default: false
|
|
62
|
+
* - Built-ins are already discoverable via the editor's native `/` autocomplete
|
|
63
|
+
* - Keeping built-ins here requires manually maintaining a list as pi evolves
|
|
64
|
+
*/
|
|
65
|
+
includeBuiltins?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ExtensionConfig = {
|
|
69
|
+
keybindings?: ExtensionKeybindingsConfig;
|
|
70
|
+
layout?: ExtensionLayoutConfig;
|
|
71
|
+
display?: ExtensionDisplayConfig;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DEFAULT_CONFIG: Required<ExtensionConfig> = {
|
|
75
|
+
keybindings: {
|
|
76
|
+
toggle: "ctrl+shift+/",
|
|
77
|
+
scrollUp: "ctrl+shift+up",
|
|
78
|
+
scrollDown: "ctrl+shift+down",
|
|
79
|
+
scrollPageUp: null,
|
|
80
|
+
scrollPageDown: null,
|
|
81
|
+
},
|
|
82
|
+
layout: {
|
|
83
|
+
height: null,
|
|
84
|
+
},
|
|
85
|
+
display: {
|
|
86
|
+
includeBuiltins: false,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function loadConfig(): Required<ExtensionConfig> {
|
|
91
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
const configPath = path.join(dir, "config.json");
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(configPath)) {
|
|
95
|
+
return DEFAULT_CONFIG;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
100
|
+
const keybindings = {
|
|
101
|
+
...DEFAULT_CONFIG.keybindings,
|
|
102
|
+
...(parsed.keybindings ?? {}),
|
|
103
|
+
};
|
|
104
|
+
const layout = {
|
|
105
|
+
...DEFAULT_CONFIG.layout,
|
|
106
|
+
...(parsed.layout ?? {}),
|
|
107
|
+
};
|
|
108
|
+
const display = {
|
|
109
|
+
...DEFAULT_CONFIG.display,
|
|
110
|
+
...(parsed.display ?? {}),
|
|
111
|
+
};
|
|
112
|
+
return { keybindings, layout, display };
|
|
113
|
+
} catch {
|
|
114
|
+
// If config is invalid, fall back to defaults rather than breaking the session
|
|
115
|
+
return DEFAULT_CONFIG;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function visLen(s: string): number {
|
|
120
|
+
return visibleWidth(s);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function padRight(s: string, width: number): string {
|
|
124
|
+
const visible = visLen(s);
|
|
125
|
+
const padding = Math.max(0, width - visible);
|
|
126
|
+
return s + " ".repeat(padding);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeColumns(items: string[], colWidth: number, maxCols: number): string[] {
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
for (let i = 0; i < items.length; i += maxCols) {
|
|
132
|
+
const row = items.slice(i, i + maxCols);
|
|
133
|
+
lines.push(row.map((s) => padRight(s, colWidth)).join(""));
|
|
134
|
+
}
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clamp(n: number, min: number, max: number): number {
|
|
139
|
+
return Math.max(min, Math.min(max, n));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function truncatePlain(s: string, maxVisibleChars: number): string {
|
|
143
|
+
if (s.length <= maxVisibleChars) return s;
|
|
144
|
+
if (maxVisibleChars <= 1) return "…";
|
|
145
|
+
return s.slice(0, maxVisibleChars - 1) + "…";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildAllLines(width: number, commands: SlashCommandInfo[], options: { includeBuiltins: boolean }): string[] {
|
|
149
|
+
const lines: string[] = [];
|
|
150
|
+
const g = (s: string) => `\x1b[32m${s}\x1b[0m`; // green
|
|
151
|
+
const c = (s: string) => `\x1b[36m${s}\x1b[0m`; // cyan
|
|
152
|
+
const y = (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
|
|
153
|
+
const b = (s: string) => `\x1b[1m${s}\x1b[0m`; // bold
|
|
154
|
+
|
|
155
|
+
const usableWidth = Math.max(60, width - 6);
|
|
156
|
+
|
|
157
|
+
const builtins = BUILTIN_COMMANDS;
|
|
158
|
+
const extensions = commands.filter((command) => command.source === "extension").map((command) => `/${command.name}`);
|
|
159
|
+
const prompts = commands.filter((command) => command.source === "prompt").map((command) => `/${command.name}`);
|
|
160
|
+
const skills = commands.filter((command) => command.source === "skill").map((command) => `/${command.name}`);
|
|
161
|
+
|
|
162
|
+
// Order: extensions -> prompts -> skills -> builtins (optional)
|
|
163
|
+
|
|
164
|
+
lines.push(y(b(`EXTENSIONS (${extensions.length})`)));
|
|
165
|
+
{
|
|
166
|
+
const maxItemLen = extensions.length > 0 ? Math.max(...extensions.map((s) => s.length)) : 0;
|
|
167
|
+
const colWidth = clamp(maxItemLen + 2, 15, 34);
|
|
168
|
+
const cols = Math.min(6, Math.max(1, Math.floor(usableWidth / colWidth)));
|
|
169
|
+
const items = extensions.map((s) => g(truncatePlain(s, colWidth - 1)));
|
|
170
|
+
for (const line of makeColumns(items, colWidth, cols)) {
|
|
171
|
+
lines.push(" " + line);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
|
|
176
|
+
lines.push(y(b(`PROMPTS (${prompts.length})`)));
|
|
177
|
+
if (prompts.length > 0) {
|
|
178
|
+
const maxItemLen = Math.max(...prompts.map((s) => s.length));
|
|
179
|
+
const colWidth = clamp(maxItemLen + 2, 18, 40);
|
|
180
|
+
const cols = Math.max(1, Math.floor(usableWidth / colWidth));
|
|
181
|
+
const items = prompts.map((s) => c(truncatePlain(s, colWidth - 1)));
|
|
182
|
+
for (const line of makeColumns(items, colWidth, cols)) {
|
|
183
|
+
lines.push(" " + line);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
lines.push(y(b(`SKILLS (${skills.length})`)));
|
|
189
|
+
if (skills.length > 0) {
|
|
190
|
+
const maxItemLen = Math.max(...skills.map((s) => s.length));
|
|
191
|
+
const colWidth = clamp(maxItemLen + 2, 18, 40);
|
|
192
|
+
const cols = Math.max(1, Math.floor(usableWidth / colWidth));
|
|
193
|
+
const items = skills.map((s) => c(truncatePlain(s, colWidth - 1)));
|
|
194
|
+
for (const line of makeColumns(items, colWidth, cols)) {
|
|
195
|
+
lines.push(" " + line);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (options.includeBuiltins) {
|
|
199
|
+
lines.push("");
|
|
200
|
+
|
|
201
|
+
lines.push(y(b(`BUILT-IN (${builtins.length})`)));
|
|
202
|
+
{
|
|
203
|
+
const maxItemLen = builtins.length > 0 ? Math.max(...builtins.map((s) => s.length)) : 0;
|
|
204
|
+
const colWidth = clamp(maxItemLen + 2, 14, 24);
|
|
205
|
+
const cols = Math.min(7, Math.max(1, Math.floor(usableWidth / colWidth)));
|
|
206
|
+
const items = builtins.map((s) => g(truncatePlain(s, colWidth - 1)));
|
|
207
|
+
for (const line of makeColumns(items, colWidth, cols)) {
|
|
208
|
+
lines.push(" " + line);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type WidgetTheme = {
|
|
217
|
+
fg: (style: string, text: string) => string;
|
|
218
|
+
bold: (text: string) => string;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
type WidgetTui = {
|
|
222
|
+
height?: number;
|
|
223
|
+
requestRender: () => void;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function prettyKeybinding(key: string | null | undefined): string {
|
|
227
|
+
if (!key) return "(unbound)";
|
|
228
|
+
|
|
229
|
+
// make a few things more readable
|
|
230
|
+
return key
|
|
231
|
+
.replaceAll("pageUp", "PgUp")
|
|
232
|
+
.replaceAll("pageDown", "PgDn")
|
|
233
|
+
.replaceAll("shift+", "Shift+")
|
|
234
|
+
.replaceAll("alt+", "Alt+")
|
|
235
|
+
.replaceAll("ctrl+", "Ctrl+")
|
|
236
|
+
.replaceAll("up", "↑")
|
|
237
|
+
.replaceAll("down", "↓")
|
|
238
|
+
.replaceAll("left", "←")
|
|
239
|
+
.replaceAll("right", "→");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class CommandCenterWidget {
|
|
243
|
+
private tui: WidgetTui;
|
|
244
|
+
private theme: WidgetTheme;
|
|
245
|
+
private pi: ExtensionAPI;
|
|
246
|
+
private config: Required<ExtensionConfig>;
|
|
247
|
+
|
|
248
|
+
private scroll: number = 0;
|
|
249
|
+
private cachedWidth: number = 0;
|
|
250
|
+
private cachedLines: string[] = [];
|
|
251
|
+
|
|
252
|
+
constructor(tui: WidgetTui, theme: WidgetTheme, pi: ExtensionAPI, config: Required<ExtensionConfig>) {
|
|
253
|
+
this.tui = tui;
|
|
254
|
+
this.theme = theme;
|
|
255
|
+
this.pi = pi;
|
|
256
|
+
this.config = config;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
updateTheme(theme: WidgetTheme): void {
|
|
260
|
+
this.theme = theme;
|
|
261
|
+
this.invalidate();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
updateConfig(config: Required<ExtensionConfig>): void {
|
|
265
|
+
this.config = config;
|
|
266
|
+
this.invalidate();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
invalidate(): void {
|
|
270
|
+
this.cachedWidth = 0;
|
|
271
|
+
this.cachedLines = [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
scrollBy(delta: number): void {
|
|
275
|
+
this.scroll += delta;
|
|
276
|
+
this.tui.requestRender();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render(width: number): string[] {
|
|
280
|
+
const terminalHeight = this.tui.height ?? 54;
|
|
281
|
+
|
|
282
|
+
// Keep at least a few rows for the editor
|
|
283
|
+
const maxAllowedHeight = Math.max(10, terminalHeight - 6);
|
|
284
|
+
|
|
285
|
+
const configuredHeight = this.config.layout.height;
|
|
286
|
+
const height = configuredHeight
|
|
287
|
+
? clamp(Math.floor(configuredHeight), 6, maxAllowedHeight)
|
|
288
|
+
: clamp(Math.floor(terminalHeight * 0.35) + 2, 10, Math.min(18, maxAllowedHeight));
|
|
289
|
+
const innerHeight = Math.max(3, height - 4);
|
|
290
|
+
|
|
291
|
+
if (width !== this.cachedWidth) {
|
|
292
|
+
this.cachedLines = buildAllLines(width, this.pi.getCommands(), {
|
|
293
|
+
includeBuiltins: this.config.display.includeBuiltins,
|
|
294
|
+
});
|
|
295
|
+
this.cachedWidth = width;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const maxScroll = Math.max(0, this.cachedLines.length - innerHeight);
|
|
299
|
+
this.scroll = clamp(this.scroll, 0, maxScroll);
|
|
300
|
+
|
|
301
|
+
const output: string[] = [];
|
|
302
|
+
|
|
303
|
+
const toggleKey = prettyKeybinding(this.config.keybindings.toggle);
|
|
304
|
+
const scrollUpKey = prettyKeybinding(this.config.keybindings.scrollUp);
|
|
305
|
+
const scrollDownKey = prettyKeybinding(this.config.keybindings.scrollDown);
|
|
306
|
+
|
|
307
|
+
const builtinHint = this.config.display.includeBuiltins
|
|
308
|
+
? "built-ins included"
|
|
309
|
+
: "built-ins: type / in editor";
|
|
310
|
+
|
|
311
|
+
const header =
|
|
312
|
+
this.theme.fg("accent", this.theme.bold("COMMAND CENTER")) +
|
|
313
|
+
this.theme.fg("dim", ` (toggle ${toggleKey}, scroll ${scrollUpKey}/${scrollDownKey}; ${builtinHint})`);
|
|
314
|
+
|
|
315
|
+
output.push(this.theme.fg("dim", "┌" + "─".repeat(width - 2) + "┐"));
|
|
316
|
+
output.push(
|
|
317
|
+
this.theme.fg("dim", "│ ") +
|
|
318
|
+
truncateToWidth(header, width - 4, "…", true) +
|
|
319
|
+
this.theme.fg("dim", " │"),
|
|
320
|
+
);
|
|
321
|
+
output.push(this.theme.fg("dim", "├" + "─".repeat(width - 2) + "┤"));
|
|
322
|
+
|
|
323
|
+
const visible = this.cachedLines.slice(this.scroll, this.scroll + innerHeight);
|
|
324
|
+
for (const line of visible) {
|
|
325
|
+
const content = truncateToWidth(line, width - 4, "…", true);
|
|
326
|
+
output.push(this.theme.fg("dim", "│ ") + content + this.theme.fg("dim", " │"));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (let i = visible.length; i < innerHeight; i++) {
|
|
330
|
+
output.push(this.theme.fg("dim", "│") + " ".repeat(width - 2) + this.theme.fg("dim", "│"));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const scrollInfo =
|
|
334
|
+
maxScroll > 0 ? ` ${this.scroll + 1}-${this.scroll + visible.length}/${this.cachedLines.length} ` : "";
|
|
335
|
+
const footerPad = Math.max(0, width - 2 - scrollInfo.length);
|
|
336
|
+
output.push(
|
|
337
|
+
this.theme.fg(
|
|
338
|
+
"dim",
|
|
339
|
+
"└" +
|
|
340
|
+
"─".repeat(Math.floor(footerPad / 2)) +
|
|
341
|
+
scrollInfo +
|
|
342
|
+
"─".repeat(Math.ceil(footerPad / 2)) +
|
|
343
|
+
"┘",
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
return output.slice(0, height);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default function commandCenterExtension(pi: ExtensionAPI): void {
|
|
352
|
+
const WIDGET_ID = "command-center";
|
|
353
|
+
|
|
354
|
+
let widget: CommandCenterWidget | undefined;
|
|
355
|
+
let visible = false;
|
|
356
|
+
|
|
357
|
+
const readConfigAndUpdateWidget = () => {
|
|
358
|
+
const config = loadConfig();
|
|
359
|
+
if (widget) {
|
|
360
|
+
widget.updateConfig(config);
|
|
361
|
+
}
|
|
362
|
+
return config;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const show = (ctx: ExtensionContext) => {
|
|
366
|
+
const config = readConfigAndUpdateWidget();
|
|
367
|
+
|
|
368
|
+
ctx.ui.setWidget(
|
|
369
|
+
WIDGET_ID,
|
|
370
|
+
(tui, theme) => {
|
|
371
|
+
if (!widget) {
|
|
372
|
+
widget = new CommandCenterWidget(
|
|
373
|
+
tui as unknown as WidgetTui,
|
|
374
|
+
theme as unknown as WidgetTheme,
|
|
375
|
+
pi,
|
|
376
|
+
config,
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
widget.updateTheme(theme as unknown as WidgetTheme);
|
|
380
|
+
widget.updateConfig(config);
|
|
381
|
+
}
|
|
382
|
+
return widget as any;
|
|
383
|
+
},
|
|
384
|
+
{ placement: "aboveEditor" },
|
|
385
|
+
);
|
|
386
|
+
visible = true;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const hide = (ctx: ExtensionContext) => {
|
|
390
|
+
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
391
|
+
visible = false;
|
|
392
|
+
widget = undefined;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const toggle = (ctx: ExtensionContext) => {
|
|
396
|
+
if (visible) {
|
|
397
|
+
hide(ctx);
|
|
398
|
+
} else {
|
|
399
|
+
show(ctx);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
pi.registerCommand("command-center", {
|
|
404
|
+
description: "Toggle command center widget",
|
|
405
|
+
handler: async (_args, ctx) => {
|
|
406
|
+
toggle(ctx);
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Shortcut bindings from config.json
|
|
411
|
+
const config = loadConfig();
|
|
412
|
+
|
|
413
|
+
const registerIfSet = (
|
|
414
|
+
key: string | null | undefined,
|
|
415
|
+
description: string,
|
|
416
|
+
handler: (ctx: ExtensionContext) => void,
|
|
417
|
+
) => {
|
|
418
|
+
if (!key) return;
|
|
419
|
+
pi.registerShortcut(key as any, { description, handler });
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
registerIfSet(config.keybindings.toggle, "Toggle command center widget", toggle);
|
|
423
|
+
|
|
424
|
+
registerIfSet(config.keybindings.scrollUp, "Scroll command center up", () => {
|
|
425
|
+
if (!visible || !widget) return;
|
|
426
|
+
widget.scrollBy(-1);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
registerIfSet(config.keybindings.scrollDown, "Scroll command center down", () => {
|
|
430
|
+
if (!visible || !widget) return;
|
|
431
|
+
widget.scrollBy(1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
registerIfSet(config.keybindings.scrollPageUp, "Scroll command center up (page)", () => {
|
|
435
|
+
if (!visible || !widget) return;
|
|
436
|
+
widget.scrollBy(-10);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
registerIfSet(config.keybindings.scrollPageDown, "Scroll command center down (page)", () => {
|
|
440
|
+
if (!visible || !widget) return;
|
|
441
|
+
widget.scrollBy(10);
|
|
442
|
+
});
|
|
443
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-command-center",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scrollable widget displaying available /commands from extensions/prompts/skills, compatible with simultaneous editor use.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"commands",
|
|
10
|
+
"widget"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/w-winter/dot314.git",
|
|
16
|
+
"directory": "packages/pi-command-center"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/w-winter/dot314/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/w-winter/dot314#readme",
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"extensions/command-center/index.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@mariozechner/pi-coding-agent": ">=0.51.0",
|
|
29
|
+
"@mariozechner/pi-tui": "*"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"prepack": "node ../../scripts/pi-package-prepack.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"extensions/**",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"package.json"
|
|
39
|
+
],
|
|
40
|
+
"dot314Prepack": {
|
|
41
|
+
"copy": [
|
|
42
|
+
{
|
|
43
|
+
"from": "../../extensions/command-center/index.ts",
|
|
44
|
+
"to": "extensions/command-center/index.ts"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"from": "../../extensions/command-center/config.json.example",
|
|
48
|
+
"to": "extensions/command-center/config.json.example"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"from": "../../extensions/command-center/README.md",
|
|
52
|
+
"to": "extensions/command-center/README.md"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"from": "../../LICENSE",
|
|
56
|
+
"to": "LICENSE"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|