@d3ara1n/pi-editor-shell 0.4.0 → 0.6.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,30 +1,43 @@
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%/NNk|N.NM · 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
 
16
16
  In `~/.pi/agent/settings.json` under the `editorShell` key:
17
17
 
18
- ```jsonc
18
+ ```json
19
19
  {
20
20
  "editorShell": {
21
- // Status keys to pin to the top-right corner of the shell.
22
- // Only keys set via ctx.ui.setStatus() are eligible.
23
- "pinnedStatus": ["subagent", "access-denied"]
21
+ "pinnedStatus": ["subagent", "access-denied"],
22
+ "icons": {
23
+ "model": "robot",
24
+ "cache": "\\uf0e7"
25
+ }
24
26
  }
25
27
  }
26
28
  ```
27
29
 
30
+ ### Default icons
31
+
32
+ | Slot | Glyph | Nerd Font name |
33
+ |------|-------|----------------|
34
+ | `model` | `` | oct-cpu |
35
+ | `thinking` | `` | oct-light_bulb |
36
+ | `context` | `` | oct-cache |
37
+ | `cache` | `⚡` | oct-zap |
38
+ | `hitRate` | `` | fa-bullseye |
39
+ | `folder` | `` | fa-folder_open |
40
+
28
41
  ## Commands
29
42
 
30
43
  | Command | Description |
@@ -35,6 +48,8 @@ In `~/.pi/agent/settings.json` under the `editorShell` key:
35
48
 
36
49
  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
50
 
51
+ 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.
52
+
38
53
  ## Installation
39
54
 
40
55
  ```bash
@@ -43,7 +58,7 @@ pi install npm:@d3ara1n/pi-editor-shell
43
58
 
44
59
  Or add to `~/.pi/agent/settings.json`:
45
60
 
46
- ```jsonc
61
+ ```json
47
62
  {
48
63
  "extensions": [
49
64
  "/absolute/path/to/pi-extensions/packages/pi-editor-shell"
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-editor-shell",
3
- "version": "0.4.0",
3
+ "version": "0.6.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
8
  "pi",
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,28 @@ 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
+ function trimFixed1(n: number): string {
51
+ const text = n.toFixed(1);
52
+ return text.endsWith(".0") ? text.slice(0, -2) : text;
53
+ }
54
+
55
+ function formatContextWindow(tokens: number): string {
56
+ if (tokens >= 1_000_000) return `${trimFixed1(tokens / 1_000_000)}M`;
57
+ return `${(tokens / 1_000).toFixed(0)}k`;
58
+ }
59
+
60
+ // ── Built-in icon set (Nerd Font). Users can override any subset via the
61
+ // `editorShell.icons` config — see config.ts. `cache` uses U+26A1, which
62
+ // Nerd Fonts maps `oct-zap` to directly (no dedicated glyph), so it is
63
+ // the same glyph in and out of a Nerd Font terminal.
64
+ const DEFAULT_ICONS: EditorShellIcons = {
52
65
  model: "\uf4bc", // oct-cpu
53
- thinking: "\uf400", // oct-light-bulb
66
+ thinking: "\uf400", // oct-light_bulb
54
67
  context: "\uf49b", // oct-cache
55
- cache: "\u26a1", // ⚡ oct-zap
68
+ cache: "\u26a1", // ⚡ oct-zap (NF maps this codepoint to U+26A1)
56
69
  hitRate: "\uf140", // fa-bullseye(靶心,缓存命中率)
57
- folder: "\uf07c", // fa-folder
58
- } as const;
70
+ folder: "\uf07c", // fa-folder_open
71
+ };
59
72
 
60
73
  /** Minimal inline types to read cache-read totals without importing the
61
74
  * full pi-ai message union tree. */
@@ -192,7 +205,10 @@ export default function (pi: ExtensionAPI) {
192
205
  // The factory may run again when pi rebuilds the editor (model switch,
193
206
  // reload, …), so always drive whichever instance is current.
194
207
  let editor: CardEditor | undefined;
195
- let config: EditorShellConfig = { pinnedStatus: [] };
208
+ let config: EditorShellConfig = { pinnedStatus: [], icons: {} };
209
+ // Resolved icons for the current session: built-in defaults merged with
210
+ // the user's overrides. Re-computed at session_start.
211
+ let icons: EditorShellIcons = { ...DEFAULT_ICONS };
196
212
  // Shared footer-data ref — the provider (running inside CardEditor.render)
197
213
  // reads it to resolve pinned status keys to their current text.
198
214
  let footerSnap: FooterSnap | undefined;
@@ -241,6 +257,7 @@ export default function (pi: ExtensionAPI) {
241
257
 
242
258
  _cwd = ctx.cwd;
243
259
  config = loadEditorShellConfig(ctx.cwd);
260
+ icons = { ...DEFAULT_ICONS, ...config.icons };
244
261
  _cacheTotal = sumCacheRead(ctx);
245
262
  _latestUsage = latestAssistantUsage(ctx);
246
263
  refreshGitDirty(ctx.cwd, () => editor?.requestRender());
@@ -273,8 +290,8 @@ export default function (pi: ExtensionAPI) {
273
290
  const ctxWindow = usage?.contextWindow ?? ctx.model?.contextWindow;
274
291
  const ctxText =
275
292
  pct != null && ctxWindow
276
- ? `${pct.toFixed(1)}%/${(ctxWindow / 1000).toFixed(0)}k`
277
- : "?/??k";
293
+ ? `${pct.toFixed(1)}%/${formatContextWindow(ctxWindow)}`
294
+ : "?/??";
278
295
 
279
296
  // Cache-read tokens — per-turn figure first, session total in parens,
280
297
  // then hit rate (pi's "CHxx%" formula). All refreshed at agent_end and
@@ -283,7 +300,7 @@ export default function (pi: ExtensionAPI) {
283
300
  const hitRate = cacheHitRate(_latestUsage);
284
301
  const cachePart =
285
302
  _cacheTotal > 0
286
- ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${ICON.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${ICON.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
303
+ ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${icons.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${icons.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
287
304
  : "";
288
305
 
289
306
  // Git branch + dirty state — pi's format: ~/Projects (main).
@@ -292,16 +309,16 @@ export default function (pi: ExtensionAPI) {
292
309
  const dirty = branch ? gitDirtyDisplay() : "";
293
310
  const cwdDisplay =
294
311
  branch && branch !== "detached"
295
- ? `${ICON.folder} ${cwdText} (${branch}${dirty})`
296
- : `${ICON.folder} ${cwdText}`;
312
+ ? `${icons.folder} ${cwdText} (${branch}${dirty})`
313
+ : `${icons.folder} ${cwdText}`;
297
314
 
298
315
  // Model in accent; thinking label in its level token — same hue the
299
316
  // border takes on, so switching levels visibly retints both together.
300
317
  return {
301
- topLeft: ` ${theme.fg("accent", `${ICON.model} ${model}`)}${theme.fg("dim", " · ")}${theme.fg(thinkingColor, `${ICON.thinking} ${thinking}`)} `,
318
+ topLeft: ` ${theme.fg("accent", `${icons.model} ${model}`)}${theme.fg("dim", " · ")}${theme.fg(thinkingColor, `${icons.thinking} ${thinking}`)} `,
302
319
  topRight: buildPinned(),
303
320
  // Context in severity color; cwd stays muted so it never competes.
304
- bottomLeft: ` ${theme.fg(contextToken(pct), `${ICON.context} ${ctxText}`)}${cachePart} `,
321
+ bottomLeft: ` ${theme.fg(contextToken(pct), `${icons.context} ${ctxText}`)}${cachePart} `,
305
322
  bottomRight: theme.fg("muted", ` ${cwdDisplay} `),
306
323
  };
307
324
  };