bermudis-pi-goodies 0.5.4 → 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,7 +1,7 @@
1
1
  # bermudis-pi-goodies
2
2
 
3
3
  A bundle of small, frequently-used [Pi](https://github.com/earendil-works/pi)
4
- extensions. One entry point, nine independent features.
4
+ extensions. One entry point, ten independent features.
5
5
 
6
6
  | Feature | Command / hook | What it does |
7
7
  | ------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -10,7 +10,8 @@ extensions. One entry point, nine independent features.
10
10
  | `name-with-ai` | `/name-with-ai [name]` | Generate a short session name from the first user message (or set one manually). |
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
- | `model-thinking` | hook (no command) | Apply native scoped-model thinking levels consistently when selecting a model through the full picker. |
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
15
  | `kilo` | provider | Access Kilo Gateway models via `/login kilo` or `KILO_API_KEY`. |
15
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. |
16
17
  | `tps` | hook (no command) | Notify tokens/sec and in/out/cache token usage at the end of each agent turn. |
@@ -20,7 +21,7 @@ extensions. One entry point, nine independent features.
20
21
  After publishing the package to npm:
21
22
 
22
23
  ```bash
23
- pi install npm:bermudis-pi-goodies@0.5.4
24
+ pi install npm:bermudis-pi-goodies@0.6.0
24
25
  ```
25
26
 
26
27
  Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
@@ -29,36 +30,41 @@ Kilo's provider and its balance footer are bundled here.
29
30
 
30
31
  ## Model selection and thinking
31
32
 
32
- Pi's native model settings are the single source of truth. Add models to
33
- `enabledModels` in `~/.pi/agent/settings.json` in cycle order. Add an optional
34
- thinking level after a colon:
35
-
36
- ```json
37
- {
38
- "enabledModels": [
39
- "opencode/hy3-free:high",
40
- "zai/glm-5.2:high",
41
- "openai-codex/gpt-5.6-terra:medium"
42
- ]
43
- }
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
44
41
  ```
45
42
 
46
- Pi applies those levels while cycling with Ctrl+P and Ctrl+Shift+P. This
47
- package fills the small consistency gaps: choosing a scoped model through the
48
- full model picker and starting with a plain explicit `--model` apply its
49
- configured level. Explicit `--thinking` and `--model ...:<level>` still win
50
- (unless the `:<level>` ending is the model's registered id, in which case the
51
- scoped level applies).
52
- One gap is inherent to Pi: it emits no selection event when you pick the
53
- model that is already active, so re-selecting it in the full picker leaves a
54
- manual thinking level in place — switch models (or cycle) to snap back.
55
- Resumed and forked sessions retain the level restored by Pi; `/new` carries the
56
- previous session's model and active thinking level into the new session.
57
-
58
- Use `/scoped-models` to search, enable, disable, and reorder the cycle list.
59
- Be aware that pi 0.84.2 currently writes bare model IDs when that screen is
60
- saved, so saving there removes any `:level` suffixes; restore them in
61
- `settings.json` afterward.
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.
62
68
 
63
69
  ## Provider and balance details
64
70
 
package/index.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  * - name-with-ai /name-with-ai generate a session name via the model
9
9
  * - zed /z open Zed on cwd
10
10
  * - prefer-tools hook block legacy tools (use trash/rg/fd/uv)
11
- * - model-thinking hook apply native scoped-model thinking levels consistently
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
13
  * - kilo provider access Kilo Gateway models
13
14
  * - provider-balance footer show Kilo credits or Codex quota in the footer
14
15
  * - tps hook notify tokens/sec and usage at each agent turn end
@@ -17,6 +18,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
18
  import copyTrajectory from "./copy-trajectory.ts";
18
19
  import copyWithModel from "./copy-with-model.ts";
19
20
  import kilo from "./kilo.ts";
21
+ import keepModelOnNew from "./keep-model-on-new.ts";
20
22
  import modelThinking from "./model-thinking.ts";
21
23
  import providerBalance from "./provider-balance.ts";
22
24
  import tps from "./tps.ts";
@@ -30,6 +32,9 @@ export default function bermudisPiGoodies(pi: ExtensionAPI): void {
30
32
  nameWithAi(pi);
31
33
  zed(pi);
32
34
  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
+ keepModelOnNew(pi);
33
38
  modelThinking(pi);
34
39
  providerBalance(pi);
35
40
  kilo(pi);
@@ -0,0 +1,76 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ interface ModelIdentity {
7
+ provider: string;
8
+ id: string;
9
+ }
10
+
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.
15
+ */
16
+ const pendingModels = new Map<string, ModelIdentity>();
17
+
18
+ function handoffKey(
19
+ sessionFile: string | undefined,
20
+ ctx: Pick<ExtensionContext, "cwd">,
21
+ ): string {
22
+ return sessionFile ?? `ephemeral:${ctx.cwd}`;
23
+ }
24
+
25
+ export interface KeepModelOnNewOptions {
26
+ /** Override the process-local handoff store (tests). */
27
+ pendingModels?: Map<string, ModelIdentity>;
28
+ }
29
+
30
+ export default function keepModelOnNew(
31
+ pi: ExtensionAPI,
32
+ options: KeepModelOnNewOptions = {},
33
+ ): void {
34
+ const handoffs = options.pendingModels ?? pendingModels;
35
+
36
+ pi.on("session_before_switch", (event, ctx) => {
37
+ if (event.reason !== "new" || !ctx.model) return;
38
+ handoffs.set(handoffKey(ctx.sessionManager.getSessionFile(), ctx), {
39
+ provider: ctx.model.provider,
40
+ id: ctx.model.id,
41
+ });
42
+ });
43
+
44
+ pi.on("session_start", async (event, ctx) => {
45
+ if (event.reason !== "new") return;
46
+
47
+ const key = handoffKey(event.previousSessionFile, ctx);
48
+ const previous = handoffs.get(key);
49
+ handoffs.delete(key);
50
+ if (!previous) return;
51
+ if (
52
+ ctx.model?.provider === previous.provider &&
53
+ ctx.model.id === previous.id
54
+ ) {
55
+ return;
56
+ }
57
+
58
+ const model = ctx.modelRegistry.find(previous.provider, previous.id);
59
+ if (!model) {
60
+ const message = `Could not keep model after /new: ${previous.provider}/${previous.id} is unavailable`;
61
+ console.warn(`[keep-model-on-new] ${message}`);
62
+ ctx.ui.notify(message, "warning");
63
+ return;
64
+ }
65
+
66
+ try {
67
+ await pi.setModel(model);
68
+ } catch (error) {
69
+ const detail = error instanceof Error ? error.message : String(error);
70
+ console.warn(
71
+ `[keep-model-on-new] Failed to restore ${previous.provider}/${previous.id}: ${detail}`,
72
+ );
73
+ ctx.ui.notify(`Could not keep model after /new: ${detail}`, "warning");
74
+ }
75
+ });
76
+ }