bermudis-pi-goodies 0.6.0 → 0.7.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
@@ -11,7 +11,7 @@ extensions. One entry point, ten independent features.
11
11
  | `zed` | `/z` | Open Zed editor on the current working directory. |
12
12
  | `prefer-tools` | hook (no command) | Nudge toward modern CLIs: `rg` over `grep`, `fd` over `find`, `uv` over bare `python`/`pip`/`pytest`/`mypy`. |
13
13
  | `keep-model-on-new` | hook (no command) | Keep the active model when `/new` starts a fresh session instead of reverting to pi's saved default model. |
14
- | `model-thinking` | hook + `/levels` | Per-model thinking levels stored in an extension sidecar (pi's /scoped-models rewrites `enabledModels` bare, wiping `:level` suffixes); `/levels` edits them, hooks apply them whenever a model becomes active. |
14
+ | `clean-tui` | tool overrides (no command) | Collapse built-in tool output for a cleaner TUI: keeps a one-line call header, hides results/diffs until you expand a row with ctrl+e or click. |
15
15
  | `kilo` | provider | Access Kilo Gateway models via `/login kilo` or `KILO_API_KEY`. |
16
16
  | `provider-balance` | footer (no command) | Show remaining Kilo or OpenRouter credits, z.ai token-plan quota, or OpenAI Codex quota on the right side of the working-directory footer line. |
17
17
  | `tps` | hook (no command) | Notify tokens/sec and in/out/cache token usage at the end of each agent turn. |
@@ -21,50 +21,33 @@ extensions. One entry point, ten independent features.
21
21
  After publishing the package to npm:
22
22
 
23
23
  ```bash
24
- pi install npm:bermudis-pi-goodies@0.6.0
24
+ pi install npm:bermudis-pi-goodies@0.7.0
25
25
  ```
26
26
 
27
27
  Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
28
28
  feature is independent — disabling one is a one-line edit in `index.ts`.
29
29
  Kilo's provider and its balance footer are bundled here.
30
30
 
31
- ## Model selection and thinking
32
-
33
- Pi's native scoped-models config would put per-model thinking levels in
34
- `enabledModels` entries like `"zai/glm-5.3:high"`, but the `/scoped-models`
35
- screen (which maintains that list) rewrites it with bare model ids on every
36
- save, destroying any `:level` suffix. So this package stores levels in its
37
- own sidecar file:
38
-
39
- ```
40
- ~/.pi/agent/data/bermudis-pi-goodies/thinking-levels.json
41
- ```
42
-
43
- The split of ownership is clean: `/scoped-models` owns which models are
44
- enabled and their Ctrl+P cycle order; `/levels` owns the per-model thinking
45
- level and can never be wiped by the other screen.
46
-
47
- Use `/levels` to edit: ↑↓ moves between scoped models, ←→ cycles each
48
- model's level through the ladder it actually supports plus `inherit`
49
- (which leaves pi's global default in charge), Enter saves atomically and
50
- applies to the active model immediately, Esc cancels.
51
-
52
- Hooks apply the stored level whenever a model becomes active: full-picker
53
- selection, Ctrl+P cycling, startup, and `/new` (where this package carries
54
- the previous session's active model forward). A native scoped level for the session — via
55
- `--models "x:high"` or a hand-suffixed enabledModels entry — always wins;
56
- the sidecar only fills in where pi itself has no level. Explicit
57
- `--thinking` or `--model ...:<level>` still
58
- wins for that launched session — a model whose registered id genuinely ends
59
- in `:<level>` is indistinguishable from that shorthand without the registry,
60
- so it opts out of its stored level for that startup; switch or cycle to
61
- re-apply. Resumed and forked sessions keep the level restored by pi.
62
-
63
- One gap is inherent to Pi: it emits no `model_select` when you pick the
64
- model that is already active, so re-selecting it in the full picker leaves
65
- a manual thinking level in place — switch models (or cycle) to snap back.
66
- Saving the `/scoped-models` screen in pi 0.84.2 writes bare model ids, which
67
- is now harmless: bare ids are exactly what this design expects.
31
+ ## Per-model thinking levels (retired in favor of Pi 0.84.3)
32
+
33
+ This package previously shipped a `model-thinking` module with a `/levels`
34
+ command and an extension-owned sidecar at
35
+ `~/.pi/agent/data/bermudis-pi-goodies/thinking-levels.json`. It existed
36
+ because Pi's `/scoped-models` screen rewrote `enabledModels` with bare model
37
+ ids (wiping any `:level` suffix) and because every `setThinkingLevel()` call
38
+ persisted as the global default.
39
+
40
+ Pi [0.84.3](https://github.com/earendil-works/pi/releases/tag/v0.84.3) fixed
41
+ both: in-session model and thinking changes are now ephemeral by default
42
+ (persist only via `/settings` or Ctrl+S, [#5263](https://github.com/earendil-works/pi/issues/5263)),
43
+ and Pi added a native per-model thinking-level override keyed by
44
+ `provider/modelId`, stored in `settings.json` `modelThinkingLevels` and
45
+ edited via `/settings` → "Default thinking level per model". That is exactly
46
+ what `model-thinking` provided, so the module is retired.
47
+
48
+ If you had levels in the old sidecar
49
+ (`~/.pi/agent/data/bermudis-pi-goodies/thinking-levels.json`), re-enter them
50
+ via `/settings` → "Default thinking level per model".
68
51
 
69
52
  ## Provider and balance details
70
53
 
package/clean-tui.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Collapse built-in tool output for a cleaner TUI focused on agent prose.
3
+ *
4
+ * Re-registers pi's built-in tools (read, bash, edit, write, find, grep, ls)
5
+ * with the same names, delegating `execute()` to the original factory
6
+ * implementations so behavior is unchanged. The only override is rendering:
7
+ * each tool keeps a terse one-line call header (command, path, or pattern)
8
+ * but produces no visible output when collapsed. Expand a row with ctrl+e or
9
+ * click to see the full result/diff.
10
+ *
11
+ * This is the display target the user chose: hide output, keep the call line.
12
+ * No global fold toggle — folding stays per-row via pi's native expand.
13
+ */
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ createBashTool,
17
+ createEditTool,
18
+ createFindTool,
19
+ createGrepTool,
20
+ createLsTool,
21
+ createReadTool,
22
+ createWriteTool,
23
+ } from "@earendil-works/pi-coding-agent";
24
+ import { Text } from "@earendil-works/pi-tui";
25
+ import { homedir } from "node:os";
26
+
27
+ type BuiltInTools = {
28
+ read: ReturnType<typeof createReadTool>;
29
+ bash: ReturnType<typeof createBashTool>;
30
+ edit: ReturnType<typeof createEditTool>;
31
+ write: ReturnType<typeof createWriteTool>;
32
+ find: ReturnType<typeof createFindTool>;
33
+ grep: ReturnType<typeof createGrepTool>;
34
+ ls: ReturnType<typeof createLsTool>;
35
+ };
36
+
37
+ const toolCache = new Map<string, BuiltInTools>();
38
+
39
+ function createBuiltInTools(cwd: string): BuiltInTools {
40
+ return {
41
+ read: createReadTool(cwd),
42
+ bash: createBashTool(cwd),
43
+ edit: createEditTool(cwd),
44
+ write: createWriteTool(cwd),
45
+ find: createFindTool(cwd),
46
+ grep: createGrepTool(cwd),
47
+ ls: createLsTool(cwd),
48
+ };
49
+ }
50
+
51
+ function getBuiltInTools(cwd: string): BuiltInTools {
52
+ let tools = toolCache.get(cwd);
53
+ if (!tools) {
54
+ tools = createBuiltInTools(cwd);
55
+ toolCache.set(cwd, tools);
56
+ }
57
+ return tools;
58
+ }
59
+
60
+ function shortenPath(path: string): string {
61
+ const home = homedir();
62
+ if (path.startsWith(home)) return `~${path.slice(home.length)}`;
63
+ return path;
64
+ }
65
+
66
+ /** Extract the text content from a tool result, or undefined if none. */
67
+ function resultText(result: {
68
+ content: Array<{ type: string; text?: string }>;
69
+ }): string | undefined {
70
+ const textContent = result.content.find((c) => c.type === "text");
71
+ return textContent?.type === "text" ? textContent.text : undefined;
72
+ }
73
+
74
+ export default function cleanTui(pi: ExtensionAPI): void {
75
+ // parameters is a static TypeBox schema; any cwd instance yields the same
76
+ // schema. execute() uses ctx.cwd for the real working directory.
77
+ const schemaTools = getBuiltInTools(process.cwd());
78
+
79
+ pi.registerTool({
80
+ name: "read",
81
+ label: "read",
82
+ description: schemaTools.read.description,
83
+ parameters: schemaTools.read.parameters,
84
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
85
+ return getBuiltInTools(ctx.cwd).read.execute(
86
+ toolCallId,
87
+ params,
88
+ signal,
89
+ onUpdate,
90
+ );
91
+ },
92
+ renderCall(args, theme) {
93
+ const path = shortenPath(args.path || "");
94
+ let display = path
95
+ ? theme.fg("accent", path)
96
+ : theme.fg("toolOutput", "...");
97
+ if (args.offset !== undefined || args.limit !== undefined) {
98
+ const start = args.offset ?? 1;
99
+ const end = args.limit !== undefined ? start + args.limit - 1 : "";
100
+ display += theme.fg("warning", `:${start}${end ? `-${end}` : ""}`);
101
+ }
102
+ return new Text(
103
+ `${theme.fg("toolTitle", theme.bold("read"))} ${display}`,
104
+ 0,
105
+ 0,
106
+ );
107
+ },
108
+ renderResult(result, { expanded }, theme) {
109
+ if (!expanded) return new Text("", 0, 0);
110
+ const text = resultText(result);
111
+ if (!text) return new Text("", 0, 0);
112
+ return new Text(
113
+ `\n${text
114
+ .split("\n")
115
+ .map((l) => theme.fg("toolOutput", l))
116
+ .join("\n")}`,
117
+ 0,
118
+ 0,
119
+ );
120
+ },
121
+ });
122
+
123
+ pi.registerTool({
124
+ name: "bash",
125
+ label: "bash",
126
+ description: schemaTools.bash.description,
127
+ parameters: schemaTools.bash.parameters,
128
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
129
+ return getBuiltInTools(ctx.cwd).bash.execute(
130
+ toolCallId,
131
+ params,
132
+ signal,
133
+ onUpdate,
134
+ );
135
+ },
136
+ renderCall(args, theme) {
137
+ const command = args.command || "...";
138
+ const timeout = args.timeout as number | undefined;
139
+ const suffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
140
+ return new Text(
141
+ theme.fg("toolTitle", theme.bold(`$ ${command}`)) + suffix,
142
+ 0,
143
+ 0,
144
+ );
145
+ },
146
+ renderResult(result, { expanded }, theme) {
147
+ if (!expanded) return new Text("", 0, 0);
148
+ const text = resultText(result)?.trim();
149
+ if (!text) return new Text("", 0, 0);
150
+ return new Text(
151
+ `\n${text
152
+ .split("\n")
153
+ .map((l) => theme.fg("toolOutput", l))
154
+ .join("\n")}`,
155
+ 0,
156
+ 0,
157
+ );
158
+ },
159
+ });
160
+
161
+ pi.registerTool({
162
+ name: "write",
163
+ label: "write",
164
+ description: schemaTools.write.description,
165
+ parameters: schemaTools.write.parameters,
166
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
167
+ return getBuiltInTools(ctx.cwd).write.execute(
168
+ toolCallId,
169
+ params,
170
+ signal,
171
+ onUpdate,
172
+ );
173
+ },
174
+ renderCall(args, theme) {
175
+ const path = shortenPath(args.path || "");
176
+ const display = path
177
+ ? theme.fg("accent", path)
178
+ : theme.fg("toolOutput", "...");
179
+ const lines = args.content ? args.content.split("\n").length : 0;
180
+ const info = lines > 0 ? theme.fg("muted", ` (${lines} lines)`) : "";
181
+ return new Text(
182
+ `${theme.fg("toolTitle", theme.bold("write"))} ${display}${info}`,
183
+ 0,
184
+ 0,
185
+ );
186
+ },
187
+ renderResult(result, { expanded }, theme) {
188
+ if (!expanded) return new Text("", 0, 0);
189
+ const text = resultText(result);
190
+ if (text) return new Text(`\n${theme.fg("error", text)}`, 0, 0);
191
+ return new Text("", 0, 0);
192
+ },
193
+ });
194
+
195
+ pi.registerTool({
196
+ name: "edit",
197
+ label: "edit",
198
+ description: schemaTools.edit.description,
199
+ parameters: schemaTools.edit.parameters,
200
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
201
+ return getBuiltInTools(ctx.cwd).edit.execute(
202
+ toolCallId,
203
+ params,
204
+ signal,
205
+ onUpdate,
206
+ );
207
+ },
208
+ renderCall(args, theme) {
209
+ const path = shortenPath(args.path || "");
210
+ const display = path
211
+ ? theme.fg("accent", path)
212
+ : theme.fg("toolOutput", "...");
213
+ return new Text(
214
+ `${theme.fg("toolTitle", theme.bold("edit"))} ${display}`,
215
+ 0,
216
+ 0,
217
+ );
218
+ },
219
+ renderResult(result, { expanded }, theme) {
220
+ if (!expanded) return new Text("", 0, 0);
221
+ const text = resultText(result);
222
+ if (!text) return new Text("", 0, 0);
223
+ return new Text(`\n${theme.fg("toolOutput", text)}`, 0, 0);
224
+ },
225
+ });
226
+
227
+ pi.registerTool({
228
+ name: "find",
229
+ label: "find",
230
+ description: schemaTools.find.description,
231
+ parameters: schemaTools.find.parameters,
232
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
233
+ return getBuiltInTools(ctx.cwd).find.execute(
234
+ toolCallId,
235
+ params,
236
+ signal,
237
+ onUpdate,
238
+ );
239
+ },
240
+ renderCall(args, theme) {
241
+ const pattern = args.pattern || "";
242
+ const path = shortenPath(args.path || ".");
243
+ let text = `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}`;
244
+ text += theme.fg("toolOutput", ` in ${path}`);
245
+ return new Text(text, 0, 0);
246
+ },
247
+ renderResult(result, { expanded }, theme) {
248
+ if (!expanded) return new Text("", 0, 0);
249
+ const text = resultText(result)?.trim();
250
+ if (!text) return new Text("", 0, 0);
251
+ return new Text(
252
+ `\n${text
253
+ .split("\n")
254
+ .map((l) => theme.fg("toolOutput", l))
255
+ .join("\n")}`,
256
+ 0,
257
+ 0,
258
+ );
259
+ },
260
+ });
261
+
262
+ pi.registerTool({
263
+ name: "grep",
264
+ label: "grep",
265
+ description: schemaTools.grep.description,
266
+ parameters: schemaTools.grep.parameters,
267
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
268
+ return getBuiltInTools(ctx.cwd).grep.execute(
269
+ toolCallId,
270
+ params,
271
+ signal,
272
+ onUpdate,
273
+ );
274
+ },
275
+ renderCall(args, theme) {
276
+ const pattern = args.pattern || "";
277
+ const path = shortenPath(args.path || ".");
278
+ const glob = args.glob;
279
+ let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
280
+ text += theme.fg("toolOutput", ` in ${path}`);
281
+ if (glob) text += theme.fg("toolOutput", ` (${glob})`);
282
+ return new Text(text, 0, 0);
283
+ },
284
+ renderResult(result, { expanded }, theme) {
285
+ if (!expanded) return new Text("", 0, 0);
286
+ const text = resultText(result)?.trim();
287
+ if (!text) return new Text("", 0, 0);
288
+ return new Text(
289
+ `\n${text
290
+ .split("\n")
291
+ .map((l) => theme.fg("toolOutput", l))
292
+ .join("\n")}`,
293
+ 0,
294
+ 0,
295
+ );
296
+ },
297
+ });
298
+
299
+ pi.registerTool({
300
+ name: "ls",
301
+ label: "ls",
302
+ description: schemaTools.ls.description,
303
+ parameters: schemaTools.ls.parameters,
304
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
305
+ return getBuiltInTools(ctx.cwd).ls.execute(
306
+ toolCallId,
307
+ params,
308
+ signal,
309
+ onUpdate,
310
+ );
311
+ },
312
+ renderCall(args, theme) {
313
+ const path = shortenPath(args.path || ".");
314
+ return new Text(
315
+ `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", path)}`,
316
+ 0,
317
+ 0,
318
+ );
319
+ },
320
+ renderResult(result, { expanded }, theme) {
321
+ if (!expanded) return new Text("", 0, 0);
322
+ const text = resultText(result)?.trim();
323
+ if (!text) return new Text("", 0, 0);
324
+ return new Text(
325
+ `\n${text
326
+ .split("\n")
327
+ .map((l) => theme.fg("toolOutput", l))
328
+ .join("\n")}`,
329
+ 0,
330
+ 0,
331
+ );
332
+ },
333
+ });
334
+ }
package/index.ts CHANGED
@@ -9,17 +9,24 @@
9
9
  * - zed /z open Zed on cwd
10
10
  * - prefer-tools hook block legacy tools (use trash/rg/fd/uv)
11
11
  * - keep-model hook preserve the active model across /new
12
- * - model-thinking hook manage per-model thinking levels in an extension-owned sidecar
12
+ * - clean-tui tool overrides collapse built-in tool output; keep a one-line call header, hide results/diffs until expanded
13
13
  * - kilo provider access Kilo Gateway models
14
14
  * - provider-balance footer show Kilo credits or Codex quota in the footer
15
15
  * - tps hook notify tokens/sec and usage at each agent turn end
16
+ *
17
+ * Per-model thinking levels were previously provided here by a `model-thinking`
18
+ * module with a `/levels` command and an extension-owned sidecar. Pi 0.84.3
19
+ * added native per-model thinking-level overrides (settings.json
20
+ * `modelThinkingLevels`, edited via `/settings` → "Default thinking level per
21
+ * model") and made in-session model/thinking changes ephemeral by default, so
22
+ * that module is retired.
16
23
  */
17
24
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
25
  import copyTrajectory from "./copy-trajectory.ts";
19
26
  import copyWithModel from "./copy-with-model.ts";
20
27
  import kilo from "./kilo.ts";
21
28
  import keepModelOnNew from "./keep-model-on-new.ts";
22
- import modelThinking from "./model-thinking.ts";
29
+ import cleanTui from "./clean-tui.ts";
23
30
  import providerBalance from "./provider-balance.ts";
24
31
  import tps from "./tps.ts";
25
32
  import nameWithAi from "./name-with-ai.ts";
@@ -32,10 +39,8 @@ export default function bermudisPiGoodies(pi: ExtensionAPI): void {
32
39
  nameWithAi(pi);
33
40
  zed(pi);
34
41
  preferTools(pi);
35
- // Register this before model-thinking so /new restores the model first and
36
- // model-thinking then sees and configures the final active model.
37
42
  keepModelOnNew(pi);
38
- modelThinking(pi);
43
+ cleanTui(pi);
39
44
  providerBalance(pi);
40
45
  kilo(pi);
41
46
  tps(pi);
@@ -9,11 +9,23 @@ interface ModelIdentity {
9
9
  }
10
10
 
11
11
  /**
12
- * Extension modules are cached across Pi's session-runtime replacement, while
13
- * their factories and handlers are recreated. This small handoff therefore
14
- * survives /new without writing transient state to disk.
12
+ * Pi normally caches extension modules across a session-runtime replacement,
13
+ * but that cache can be invalidated by another resource loader or /reload.
14
+ * Keep the handoff on globalThis so a fresh import in the same Pi process
15
+ * cannot lose it. The state is still transient and never written to disk.
15
16
  */
16
- const pendingModels = new Map<string, ModelIdentity>();
17
+ const pendingModelsKey = Symbol.for(
18
+ "bermudis-pi-goodies.keep-model-on-new.pending-models",
19
+ );
20
+
21
+ type GlobalWithPendingModels = typeof globalThis & {
22
+ [pendingModelsKey]?: Map<string, ModelIdentity>;
23
+ };
24
+
25
+ function getPendingModels(): Map<string, ModelIdentity> {
26
+ const globalState = globalThis as GlobalWithPendingModels;
27
+ return (globalState[pendingModelsKey] ??= new Map());
28
+ }
17
29
 
18
30
  function handoffKey(
19
31
  sessionFile: string | undefined,
@@ -31,7 +43,7 @@ export default function keepModelOnNew(
31
43
  pi: ExtensionAPI,
32
44
  options: KeepModelOnNewOptions = {},
33
45
  ): void {
34
- const handoffs = options.pendingModels ?? pendingModels;
46
+ const handoffs = options.pendingModels ?? getPendingModels();
35
47
 
36
48
  pi.on("session_before_switch", (event, ctx) => {
37
49
  if (event.reason !== "new" || !ctx.model) return;
@@ -64,7 +76,12 @@ export default function keepModelOnNew(
64
76
  }
65
77
 
66
78
  try {
67
- await pi.setModel(model);
79
+ const restored = await pi.setModel(model);
80
+ if (!restored) {
81
+ const message = `Could not keep model after /new: ${previous.provider}/${previous.id} is not authenticated`;
82
+ console.warn(`[keep-model-on-new] ${message}`);
83
+ ctx.ui.notify(message, "warning");
84
+ }
68
85
  } catch (error) {
69
86
  const detail = error instanceof Error ? error.message : String(error);
70
87
  console.warn(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bermudis-pi-goodies",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "A bundle of small, frequently-used Pi extensions.",
6
6
  "keywords": ["pi-package"],
@@ -21,10 +21,10 @@
21
21
  "format": "prettier --write \"*.ts\" \"!*.bundle.ts\""
22
22
  },
23
23
  "devDependencies": {
24
- "@earendil-works/pi-agent-core": "^0.84.0",
25
- "@earendil-works/pi-ai": "^0.84.0",
26
- "@earendil-works/pi-coding-agent": "^0.84.0",
27
- "@earendil-works/pi-tui": "^0.84.0",
24
+ "@earendil-works/pi-agent-core": "^0.84.3",
25
+ "@earendil-works/pi-ai": "^0.84.3",
26
+ "@earendil-works/pi-coding-agent": "^0.84.3",
27
+ "@earendil-works/pi-tui": "^0.84.3",
28
28
  "@marcfargas/pi-test-harness": "0.6.1",
29
29
  "esbuild": "^0.27.0",
30
30
  "prettier": "^3.8.4"