@d3ara1n/pi-editor-shell 0.3.1 → 0.5.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # pi-editor-shell
2
2
 
3
- Replaces pi's default editor and status bar with a unified rounded-corner shell drawn with box-drawing glyphs (`╭╮││╰╯`), with status info embedded in the border. No Nerd Font required for the frame itself.
3
+ Replaces pi's default editor and status bar with a unified rounded-corner shell drawn with box-drawing glyphs (`╭╮││╰╯`), with status info embedded in the border. The frame and spinner use only standard Unicode; the six border icons are Nerd Font glyphs (overridable see [Configuration](#configuration)).
4
4
 
5
5
  ## What shows up where
6
6
 
7
- - **Top border** — ` model · thinking-level ` (left) + pinned extension statuses (right, via `pinnedStatus` config)
8
- - **Bottom border** — ` ctx NN%/NNNk · cache-tokens ` (left) + ` ~current/dir (branch) ` (right, includes git branch when available)
7
+ - **Top border** — `  provider/model · thinking-level ` (left) + pinned extension statuses (right, via `pinnedStatus` config)
8
+ - **Bottom border** — ` ctx NN%/NNNk · cacheRead (total)  hitRate% ` (left) + ` ~/Projects (main +2 ~1) ` (right, shows git branch + dirty state when in a repo)
9
9
  - **Below shell** — Auto-wrapping extension status line (all `setStatus` entries not pinned to the top)
10
10
  - **Border color** follows pi's thinking-level / bash-mode indicator automatically.
11
11
 
12
- All segments are re-read from live session state on every paint, so switching thinking level or burning context updates the frame on the next render with no extra wiring.
12
+ All segments are re-read from live session state on every paint, so switching thinking level or burning context updates the frame on the next render with no extra wiring. When the agent is active, the current phase spinner (thinking/outputting/toolcall/exec) replaces the model text in the top-left slot.
13
13
 
14
14
  ## Configuration
15
15
 
@@ -20,11 +20,30 @@ In `~/.pi/agent/settings.json` under the `editorShell` key:
20
20
  "editorShell": {
21
21
  // Status keys to pin to the top-right corner of the shell.
22
22
  // Only keys set via ctx.ui.setStatus() are eligible.
23
- "pinnedStatus": ["subagent", "access-denied"]
23
+ "pinnedStatus": ["subagent", "access-denied"],
24
+
25
+ // Per-slot border-icon overrides. Any subset; missing keys fall back
26
+ // to the built-in Nerd Font set (see table below). Values are raw
27
+ // characters — "\uf0e7" for a Nerd Font glyph, "🤖" for an emoji.
28
+ "icons": {
29
+ "model": "🤖",
30
+ "cache": "\uf0e7"
31
+ }
24
32
  }
25
33
  }
26
34
  ```
27
35
 
36
+ ### Default icons
37
+
38
+ | Slot | Glyph | Nerd Font name |
39
+ |------|-------|----------------|
40
+ | `model` | `` | oct-cpu |
41
+ | `thinking` | `` | oct-light_bulb |
42
+ | `context` | `` | oct-cache |
43
+ | `cache` | `⚡` | oct-zap |
44
+ | `hitRate` | `` | fa-bullseye |
45
+ | `folder` | `` | fa-folder_open |
46
+
28
47
  ## Commands
29
48
 
30
49
  | Command | Description |
@@ -35,9 +54,15 @@ In `~/.pi/agent/settings.json` under the `editorShell` key:
35
54
 
36
55
  The default pi editor only draws a horizontal line above and below the input area (no side borders), and a separate footer renders the status bar. This extension replaces both — it wraps the built-in `CustomEditor`, renders it at `width - 2`, wraps every line with left/right glyphs, and embeds the status bar information (extension statuses) below the shell. The total width is unchanged. Border color follows pi's `borderColor` (which encodes thinking level / bash mode), so the shell stays semantically consistent and reacts to theme changes automatically.
37
56
 
57
+ When the autocomplete popup is open, the divider between editor content and popup items becomes a T-junction (`├─┤`) carrying the context/cwd info, closing everything into one connected card with two panes. Below `MIN_WIDTH` (20 columns), it falls back to the default editor.
58
+
38
59
  ## Installation
39
60
 
40
- Add the package directory to the `extensions` array in `~/.pi/agent/settings.json`:
61
+ ```bash
62
+ pi install npm:@d3ara1n/pi-editor-shell
63
+ ```
64
+
65
+ Or add to `~/.pi/agent/settings.json`:
41
66
 
42
67
  ```jsonc
43
68
  {
@@ -47,8 +72,6 @@ Add the package directory to the `extensions` array in `~/.pi/agent/settings.jso
47
72
  }
48
73
  ```
49
74
 
50
- Then run `/reload` or restart pi.
51
-
52
75
  ## Caveats
53
76
 
54
77
  - **Mutually exclusive** with other editor-replacing extensions (`border-status-editor`, `rainbow-editor`, `modal-editor`, …). Disable those when enabling this one — `setEditorComponent` is last-writer-wins.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-editor-shell",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
- "description": "Replaces pi's default editor and status bar with a unified rounded-corner shell no Nerd Font required",
5
+ "description": "Replaces pi's default editor and status bar with a unified rounded-corner shell embedding status info in the border",
6
6
  "keywords": [
7
7
  "pi-package",
8
- "pi"
8
+ "pi",
9
+ "pi-extension"
9
10
  ],
10
11
  "main": "src/index.ts",
11
12
  "peerDependencies": {
@@ -15,7 +16,8 @@
15
16
  "pi": {
16
17
  "extensions": [
17
18
  "./src/index.ts"
18
- ]
19
+ ],
20
+ "image": "https://raw.githubusercontent.com/d3ara1n/pi-extensions/main/packages/pi-editor-shell/preview.png"
19
21
  },
20
22
  "license": "MIT",
21
23
  "repository": {
package/preview.png ADDED
Binary file
package/src/config.ts CHANGED
@@ -10,16 +10,55 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
 
13
+ /** Border icon slots that users can override. Each holds a single glyph
14
+ * (Nerd Font codepoint, Unicode symbol, or emoji) — whatever the user's
15
+ * terminal can render. Defaults live next to the renderer in index.ts. */
16
+ export interface EditorShellIcons {
17
+ model: string;
18
+ thinking: string;
19
+ context: string;
20
+ cache: string;
21
+ hitRate: string;
22
+ folder: string;
23
+ }
24
+
13
25
  export interface EditorShellConfig {
14
26
  /**
15
27
  * Status keys to pin to the shell's top-right corner.
16
28
  * Only keys set via ctx.ui.setStatus() are eligible.
17
29
  */
18
30
  pinnedStatus: string[];
31
+ /**
32
+ * Per-slot border-icon overrides. Any subset; missing keys fall back to
33
+ * the built-in Nerd Font set. Values are raw characters — JSON `"\uf0e7"`
34
+ * for a Nerd Font glyph, or `"🤖"` for an emoji, etc.
35
+ */
36
+ icons: Partial<EditorShellIcons>;
37
+ }
38
+
39
+ const ICON_KEYS: ReadonlyArray<keyof EditorShellIcons> = [
40
+ "model",
41
+ "thinking",
42
+ "context",
43
+ "cache",
44
+ "hitRate",
45
+ "folder",
46
+ ];
47
+
48
+ /** Keep only known icon slots with string values — silently drops typos and
49
+ * wrong-typed entries so a bad config never crashes the renderer. */
50
+ function filterIcons(obj: Record<string, unknown>): Partial<EditorShellIcons> {
51
+ const out: Partial<EditorShellIcons> = {};
52
+ for (const key of ICON_KEYS) {
53
+ const v = obj[key];
54
+ if (typeof v === "string") out[key] = v;
55
+ }
56
+ return out;
19
57
  }
20
58
 
21
59
  export const DEFAULT_CONFIG: EditorShellConfig = {
22
60
  pinnedStatus: [],
61
+ icons: {},
23
62
  };
24
63
 
25
64
  function getAgentDir(): string {
@@ -51,9 +90,14 @@ export function loadEditorShellConfig(cwd?: string): EditorShellConfig {
51
90
  if (!raw) return { ...DEFAULT_CONFIG };
52
91
 
53
92
  const pinned = raw.pinnedStatus;
93
+ const iconsRaw = raw.icons;
54
94
  return {
55
95
  pinnedStatus: Array.isArray(pinned)
56
96
  ? pinned.filter((k): k is string => typeof k === "string")
57
97
  : DEFAULT_CONFIG.pinnedStatus,
98
+ icons:
99
+ iconsRaw && typeof iconsRaw === "object"
100
+ ? filterIcons(iconsRaw as Record<string, unknown>)
101
+ : {},
58
102
  };
59
103
  }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { CardEditor, type FrameProvider, type SpinnerPhase } from "./card-editor";
7
- import { loadEditorShellConfig, type EditorShellConfig } from "./config";
7
+ import { loadEditorShellConfig, type EditorShellConfig, type EditorShellIcons } from "./config";
8
8
 
9
9
  /**
10
10
  * pi-editor-shell — Replaces pi's default editor and status bar with a
@@ -47,15 +47,18 @@ function contextToken(pct: number | null | undefined): ThemeColor {
47
47
  return "success";
48
48
  }
49
49
 
50
- // ── Nerd Font icons (Octicons + FontAwesome) ──────────────────────────
51
- const ICON = {
50
+ // ── Built-in icon set (Nerd Font). Users can override any subset via the
51
+ // `editorShell.icons` config see config.ts. `cache` uses U+26A1, which
52
+ // Nerd Fonts maps `oct-zap` to directly (no dedicated glyph), so it is
53
+ // the same glyph in and out of a Nerd Font terminal.
54
+ const DEFAULT_ICONS: EditorShellIcons = {
52
55
  model: "\uf4bc", // oct-cpu
53
- thinking: "\uf400", // oct-light-bulb
56
+ thinking: "\uf400", // oct-light_bulb
54
57
  context: "\uf49b", // oct-cache
55
- cache: "\u26a1", // ⚡ oct-zap
58
+ cache: "\u26a1", // ⚡ oct-zap (NF maps this codepoint to U+26A1)
56
59
  hitRate: "\uf140", // fa-bullseye(靶心,缓存命中率)
57
- folder: "\uf07c", // fa-folder
58
- } as const;
60
+ folder: "\uf07c", // fa-folder_open
61
+ };
59
62
 
60
63
  /** Minimal inline types to read cache-read totals without importing the
61
64
  * full pi-ai message union tree. */
@@ -192,7 +195,10 @@ export default function (pi: ExtensionAPI) {
192
195
  // The factory may run again when pi rebuilds the editor (model switch,
193
196
  // reload, …), so always drive whichever instance is current.
194
197
  let editor: CardEditor | undefined;
195
- let config: EditorShellConfig = { pinnedStatus: [] };
198
+ let config: EditorShellConfig = { pinnedStatus: [], icons: {} };
199
+ // Resolved icons for the current session: built-in defaults merged with
200
+ // the user's overrides. Re-computed at session_start.
201
+ let icons: EditorShellIcons = { ...DEFAULT_ICONS };
196
202
  // Shared footer-data ref — the provider (running inside CardEditor.render)
197
203
  // reads it to resolve pinned status keys to their current text.
198
204
  let footerSnap: FooterSnap | undefined;
@@ -241,6 +247,7 @@ export default function (pi: ExtensionAPI) {
241
247
 
242
248
  _cwd = ctx.cwd;
243
249
  config = loadEditorShellConfig(ctx.cwd);
250
+ icons = { ...DEFAULT_ICONS, ...config.icons };
244
251
  _cacheTotal = sumCacheRead(ctx);
245
252
  _latestUsage = latestAssistantUsage(ctx);
246
253
  refreshGitDirty(ctx.cwd, () => editor?.requestRender());
@@ -283,7 +290,7 @@ export default function (pi: ExtensionAPI) {
283
290
  const hitRate = cacheHitRate(_latestUsage);
284
291
  const cachePart =
285
292
  _cacheTotal > 0
286
- ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${ICON.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${ICON.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
293
+ ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${icons.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${icons.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
287
294
  : "";
288
295
 
289
296
  // Git branch + dirty state — pi's format: ~/Projects (main).
@@ -292,16 +299,16 @@ export default function (pi: ExtensionAPI) {
292
299
  const dirty = branch ? gitDirtyDisplay() : "";
293
300
  const cwdDisplay =
294
301
  branch && branch !== "detached"
295
- ? `${ICON.folder} ${cwdText} (${branch}${dirty})`
296
- : `${ICON.folder} ${cwdText}`;
302
+ ? `${icons.folder} ${cwdText} (${branch}${dirty})`
303
+ : `${icons.folder} ${cwdText}`;
297
304
 
298
305
  // Model in accent; thinking label in its level token — same hue the
299
306
  // border takes on, so switching levels visibly retints both together.
300
307
  return {
301
- topLeft: ` ${theme.fg("accent", `${ICON.model} ${model}`)}${theme.fg("dim", " · ")}${theme.fg(thinkingColor, `${ICON.thinking} ${thinking}`)} `,
308
+ topLeft: ` ${theme.fg("accent", `${icons.model} ${model}`)}${theme.fg("dim", " · ")}${theme.fg(thinkingColor, `${icons.thinking} ${thinking}`)} `,
302
309
  topRight: buildPinned(),
303
310
  // Context in severity color; cwd stays muted so it never competes.
304
- bottomLeft: ` ${theme.fg(contextToken(pct), `${ICON.context} ${ctxText}`)}${cachePart} `,
311
+ bottomLeft: ` ${theme.fg(contextToken(pct), `${icons.context} ${ctxText}`)}${cachePart} `,
305
312
  bottomRight: theme.fg("muted", ` ${cwdDisplay} `),
306
313
  };
307
314
  };
@@ -310,6 +317,10 @@ export default function (pi: ExtensionAPI) {
310
317
  ctx.ui.setWorkingVisible(false);
311
318
 
312
319
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
320
+ // pi may rebuild the editor mid-session (model switch, reload). Stop the
321
+ // previous editor's spinner timer so it can't keep firing requestRender()
322
+ // on a stale tui handle.
323
+ editor?.setSpinner(null);
313
324
  editor = new CardEditor(tui, theme, keybindings, provider);
314
325
  return editor;
315
326
  });