@luxass/pi-personal 0.1.0 → 0.1.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
@@ -10,6 +10,8 @@ pi install npm:@luxass/pi-personal
10
10
 
11
11
  ## What it does
12
12
 
13
- The package registers a `session_start` hook. When a session starts, Pi shows a notification confirming the extension loaded.
13
+ The custom header pairs an ASCII terminal-robot mascot with the project, model, and current thinking level. A rounded frame, vertical padding, and a separate shortcut row give it room on wide terminals. Narrow panes use a compact layout and drop shortcut hints before truncating them. The thinking shortcut follows your Pi keybindings.
14
14
 
15
- That is the whole of it today. The package is a home for defaults that suit one workflow, and each addition is meant to be edited in place as that workflow changes rather than configured through settings.
15
+ Run `/resources` to inspect detected context files, skill commands, and extensions. This list is inferred from files and registered commands/tools, not a complete inventory of loaded resources.
16
+
17
+ The package also includes Crow provider support, compaction continuation, Git and package-manager interceptors, and a worker configuration guard. These defaults suit one workflow and are meant to be edited in place rather than configured through settings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxass/pi-personal",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Personal Pi extensions collected in one package, tuned to a single workflow.",
5
5
  "keywords": [
6
6
  "personal",
@@ -10,8 +10,8 @@
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/luxass/pi-extensions",
14
- "directory": "packages/personal"
13
+ "url": "https://github.com/luxass/agent-extensions",
14
+ "directory": "packages/pi-personal"
15
15
  },
16
16
  "files": [
17
17
  "src",
@@ -28,7 +28,7 @@
28
28
  "@earendil-works/pi-tui": "*"
29
29
  },
30
30
  "pi": {
31
- "image": "https://raw.githubusercontent.com/luxass/pi-extensions/main/packages/personal/docs/cover.png",
31
+ "image": "https://raw.githubusercontent.com/luxass/agent-extensions/main/packages/pi-personal/docs/cover.png",
32
32
  "extensions": [
33
33
  "./src/continue-after-compaction.ts",
34
34
  "./src/git-interceptor.ts",
@@ -4,11 +4,26 @@ import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
7
- import { VERSION } from "@earendil-works/pi-coding-agent";
8
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
+ import { keyText, VERSION } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ Key,
10
+ matchesKey,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ wrapTextWithAnsi,
14
+ } from "@earendil-works/pi-tui";
9
15
 
10
- const HEADER_MAX_WIDTH = 72;
11
- const COMPACT_WIDTH = 44;
16
+ const HEADER_MAX_WIDTH = 88;
17
+ const LOGO_MIN_WIDTH = 64;
18
+ const PI_MASCOT = [
19
+ " o ",
20
+ " | ",
21
+ " .---+---.",
22
+ " | o o |",
23
+ " | \\_/ |",
24
+ " '-------'",
25
+ " / \\ ",
26
+ ];
12
27
 
13
28
  type ResourceSnapshot = {
14
29
  context: string[];
@@ -21,34 +36,6 @@ function fit(text: string, width: number): string {
21
36
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
22
37
  }
23
38
 
24
- function wrap(text: string, width: number): string[] {
25
- if (width < 1) return [""];
26
-
27
- const lines: string[] = [];
28
- let line = "";
29
- for (const word of text.split(/\s+/).filter(Boolean)) {
30
- if (word.length > width) {
31
- if (line) lines.push(line);
32
- line = "";
33
- for (let index = 0; index < word.length; index += width) {
34
- lines.push(word.slice(index, index + width));
35
- }
36
- continue;
37
- }
38
-
39
- const next = line ? `${line} ${word}` : word;
40
- if (next.length > width) {
41
- lines.push(line);
42
- line = word;
43
- } else {
44
- line = next;
45
- }
46
- }
47
-
48
- if (line) lines.push(line);
49
- return lines.length > 0 ? lines : [""];
50
- }
51
-
52
39
  function resourceName(path: string): string {
53
40
  const parts = path.replaceAll("\\", "/").split("/");
54
41
  const file = parts.at(-1) ?? path;
@@ -104,29 +91,112 @@ async function collectResources(
104
91
  };
105
92
  }
106
93
 
107
- function renderHeader(theme: Theme, width: number, project: string, model: string): string[] {
94
+ interface HeaderOptions {
95
+ theme: Theme;
96
+ width: number;
97
+ project: string;
98
+ model: Pick<NonNullable<ExtensionContext["model"]>, "provider" | "id" | "reasoning"> | undefined;
99
+ thinking: ReturnType<ExtensionAPI["getThinkingLevel"]>;
100
+ thinkingKey: string;
101
+ }
102
+
103
+ export function renderHeader({
104
+ theme,
105
+ width,
106
+ project,
107
+ model,
108
+ thinking,
109
+ thinkingKey,
110
+ }: HeaderOptions): string[] {
111
+ if (width < 1) return [""];
112
+
108
113
  const accent = (text: string) => theme.fg("accent", text);
109
- const heading = (text: string) => theme.fg("mdHeading", theme.bold(text));
110
114
  const muted = (text: string) => theme.fg("muted", text);
111
115
  const dim = (text: string) => theme.fg("dim", text);
112
- const title = `${accent("π")} ${heading("Pi")} ${dim(`v${VERSION}`)}`;
116
+ const expanded = width >= LOGO_MIN_WIDTH;
117
+ const border = (text: string) => theme.fg(expanded ? "borderMuted" : "borderAccent", text);
118
+ const version = dim(`v${VERSION}`);
119
+ const title = `${accent("π")} ${theme.bold("Pi")} ${version}`;
120
+ const indent = width >= 44 ? " " : "";
121
+ const panelWidth = Math.min(expanded ? HEADER_MAX_WIDTH : 72, width - indent.length);
122
+ const boxed = width >= 24;
123
+ const contentWidth = boxed ? panelWidth - 4 : width;
124
+ const separator = dim(" · ");
125
+ const workspace = theme.bold(theme.fg("text", project));
126
+ const modelLabel = model
127
+ ? `${width >= 44 ? dim(`${model.provider}/`) : ""}${theme.fg("text", model.id)}`
128
+ : muted("model pending");
129
+ const thinkingLabel = model?.reasoning ? `${muted("thinking ")}${accent(thinking)}` : "";
130
+ const metadata = [modelLabel, thinkingLabel].filter(Boolean).join(separator);
131
+ const line = (text = "") => `${indent}${border("│")} ${fit(text, contentWidth)} ${border("│")}`;
113
132
 
114
- if (width < COMPACT_WIDTH) {
115
- return [fit(title, width), fit(muted("/ for commands · ! for shell"), width)];
133
+ const hints = [
134
+ `${accent("/")} ${muted("commands")}`,
135
+ `${accent("!")} ${muted("shell")}`,
136
+ ...(model?.reasoning && thinkingKey
137
+ ? [`${accent(thinkingKey === "shift+tab" ? "⇧Tab" : thinkingKey)} ${muted("thinking")}`]
138
+ : []),
139
+ accent("/resources"),
140
+ ];
141
+ // Drop complete hints, not half a shortcut. Keep /resources available longest.
142
+ const hintWidth = expanded ? contentWidth - 4 : contentWidth;
143
+ while (hints.length > 1 && visibleWidth(hints.join(" ")) > hintWidth) {
144
+ hints.splice(hints.length - 2, 1);
116
145
  }
117
146
 
118
- const panelWidth = Math.min(HEADER_MAX_WIDTH, width - 4);
119
- const contentWidth = panelWidth - 2;
120
- const border = (text: string) => theme.fg("borderAccent", text);
121
- const line = (text = "") => `│${fit(` ${text}`, contentWidth)}│`;
147
+ if (expanded) {
148
+ const logoWidth = Math.max(...PI_MASCOT.map(visibleWidth));
149
+ const detailsWidth = contentWidth - logoWidth - 5;
150
+ const name = theme.bold(theme.fg("text", "Pi"));
151
+ const details = [
152
+ `${name}${" ".repeat(Math.max(1, detailsWidth - visibleWidth(name) - visibleWidth(version)))}${version}`,
153
+ "",
154
+ workspace,
155
+ modelLabel,
156
+ thinkingLabel,
157
+ "",
158
+ ];
159
+
160
+ return [
161
+ "",
162
+ `${indent}${border(`╭${"─".repeat(panelWidth - 2)}╮`)}`,
163
+ line(),
164
+ ...PI_MASCOT.map((logo, index) =>
165
+ line(
166
+ ` ${fit(theme.bold(accent(logo)), logoWidth)} ${fit(details[index] ?? "", detailsWidth)}`,
167
+ ),
168
+ ),
169
+ line(),
170
+ line(` ${hints.join(" ")}`),
171
+ line(),
172
+ `${indent}${border(`╰${"─".repeat(panelWidth - 2)}╯`)}`,
173
+ "",
174
+ ];
175
+ }
122
176
 
177
+ const session = `${workspace}${separator}${metadata}`;
178
+ const lines: string[] = [];
179
+ if (visibleWidth(session) <= contentWidth) {
180
+ lines.push(session);
181
+ } else {
182
+ lines.push(workspace);
183
+ if (visibleWidth(metadata) <= contentWidth) lines.push(metadata);
184
+ else lines.push(modelLabel, ...(thinkingLabel ? [thinkingLabel] : []));
185
+ }
186
+ if (contentWidth >= visibleWidth("/resources")) {
187
+ if (boxed) lines.push("");
188
+ lines.push(hints.join(" "));
189
+ }
190
+
191
+ if (!boxed) return ["", ...[title, ...lines].map((line) => fit(line, width)), ""];
192
+
193
+ const label = truncateToWidth(title, panelWidth - 5, "…");
123
194
  return [
124
- ` ${border(`╭${"─".repeat(panelWidth - 2)}╮`)}`,
125
- ` ${border(line(title))}`,
126
- ` ${border(line(`${muted(project)} ${dim("·")} ${dim(model)}`))}`,
127
- ` ${border(line())}`,
128
- ` ${border(line(`${accent("/")} ${muted("commands")} ${accent("!")} ${muted("shell")} ${accent("⇧Tab")} ${muted("thinking")} ${accent("/resources")} ${muted("resources")}`))}`,
129
- ` ${border(`╰${"─".repeat(panelWidth - 2)}╯`)}`,
195
+ "",
196
+ `${indent}${border("╭─ ")}${label}${border(` ${"─".repeat(panelWidth - 5 - visibleWidth(label))}╮`)}`,
197
+ ...lines.map(line),
198
+ `${indent}${border(`╰${"─".repeat(panelWidth - 2)}╯`)}`,
199
+ "",
130
200
  ];
131
201
  }
132
202
 
@@ -144,7 +214,7 @@ function renderResources(snapshot: ResourceSnapshot, theme: Theme, width: number
144
214
  const lines = [
145
215
  border(`╭${"─".repeat(panelWidth - 2)}╮`),
146
216
  line(`${accent("π")} ${heading("Resources")}`),
147
- line(dim("Loaded for this session")),
217
+ line(dim("Detected context, skill commands, and extensions")),
148
218
  line(),
149
219
  ];
150
220
 
@@ -155,7 +225,7 @@ function renderResources(snapshot: ResourceSnapshot, theme: Theme, width: number
155
225
  ];
156
226
  for (const [label, items, marker] of sections) {
157
227
  lines.push(line(heading(`${label} · ${items.length}`)));
158
- const entries = wrap(
228
+ const entries = wrapTextWithAnsi(
159
229
  items.length > 0 ? items.join(", ") : "none detected",
160
230
  Math.max(1, contentWidth - 4),
161
231
  );
@@ -177,18 +247,20 @@ export default function (pi: ExtensionAPI) {
177
247
 
178
248
  ctx.ui.setHeader((_tui, theme) => ({
179
249
  render: (width: number) =>
180
- renderHeader(
250
+ renderHeader({
181
251
  theme,
182
252
  width,
183
- basename(ctx.cwd) || "workspace",
184
- ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "model pending",
185
- ),
253
+ project: basename(ctx.cwd) || "workspace",
254
+ model: ctx.model,
255
+ thinking: pi.getThinkingLevel(),
256
+ thinkingKey: keyText("app.thinking.cycle"),
257
+ }),
186
258
  invalidate() {},
187
259
  }));
188
260
  });
189
261
 
190
262
  pi.registerCommand("resources", {
191
- description: "Show loaded context, skills, and extensions",
263
+ description: "Show detected context, skill commands, and extensions",
192
264
  handler: async (_args, ctx) => {
193
265
  if (ctx.mode !== "tui") return;
194
266
  const resources = await collectResources(pi, ctx);