@hk_net/pi-thinking-command 0.1.7 → 0.1.8

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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.8 - 2026-08-23
4
+
5
+ ### Changed
6
+
7
+ - Mark Pi's host-provided SDK packages as optional wildcard peers so npm does not install a redundant Pi SDK dependency tree.
8
+ - Verify development and tests against Pi SDK 0.84.2 while retaining runtime compatibility with Pi 0.84.1 and newer.
9
+ - Derive `/thinking` validation and autocomplete choices from the active model's supported thinking levels, refresh them on model changes, and consolidate the overlapping autocomplete providers.
10
+ - Fall back safely to `off` when malformed custom model metadata exposes no supported thinking levels, clarify the two-step `/th` autocomplete flow, and delegate mid-line completion to Pi instead of corrupting trailing text.
11
+ - Document that the individual package and GitHub bundle must not be installed together.
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `/thinking` command extension for pi
2
2
 
3
- Adds a `/thinking` slash command to pi `>=0.84.1` for changing the active thinking/reasoning level from inside a session.
3
+ Adds a `/thinking` slash command to Pi 0.84.1 or newer (tested with the current Pi 0.84.2 release) for changing the active thinking/reasoning level from inside a session.
4
4
 
5
5
  Normally in pi, changing the thinking/reasoning level means opening the settings menu, navigating to **Thinking**, and selecting the requested level. This extension is a convenience shortcut for that workflow, so you can switch levels directly with commands such as `/thinking low` or `/thinking xhigh`.
6
6
 
@@ -8,6 +8,8 @@ This is especially useful when switching between model classes. Smaller models,
8
8
 
9
9
  ## Install
10
10
 
11
+ > **Avoid duplicate installation.** Install this npm package or the GitHub bundle, not both. Loading both copies can duplicate commands, autocomplete providers, and status updates.
12
+
11
13
  Install just this extension from npm:
12
14
 
13
15
  ```bash
@@ -36,9 +38,11 @@ After installing, restart pi or run:
36
38
  ## Usage
37
39
 
38
40
  ```text
39
- /thinking [off|minimal|low|medium|high|xhigh|max]
41
+ /thinking [level]
40
42
  ```
41
43
 
44
+ Autocomplete offers only the thinking levels supported by the active model.
45
+
42
46
  Examples:
43
47
 
44
48
  ```text
@@ -50,7 +54,7 @@ Examples:
50
54
  /thinking max
51
55
  ```
52
56
 
53
- If no argument is provided, `/thinking` sets the level to `medium`.
57
+ If no argument is provided, `/thinking` sets the level to `medium` when supported, otherwise to the model's first supported level (normally `off` for a non-reasoning model).
54
58
 
55
59
  ## Levels
56
60
 
@@ -62,17 +66,16 @@ If no argument is provided, `/thinking` sets the level to `medium`.
62
66
  - `xhigh` — very high reasoning budget
63
67
  - `max` — maximum reasoning budget
64
68
 
65
- > **Note:** If the current model does not support the requested level, pi clamps to the
66
- > nearest supported level (searching both up and down the level hierarchy).
69
+ > **Note:** `/thinking` rejects levels that the active model does not support instead of silently selecting a different level.
67
70
 
68
71
  ## Features
69
72
 
70
73
  - Registers the `/thinking` command.
71
- - Provides argument completions for all supported levels.
72
- - Autocompletes `/th...` to `/thinking` without Pi's trailing-space insertion, then presents the level picker.
74
+ - Derives argument completions from Pi's model-specific capabilities and refreshes them when the active model changes.
75
+ - Autocompletes `/th...` to `/thinking` without Pi's trailing-space insertion; press Tab again to open the level picker.
73
76
  - Shows a `thinking` status item with the current level.
74
77
  - Updates the status item when the thinking level changes.
75
- - Validates input and displays an error for unknown levels.
78
+ - Validates input and displays an error for unknown or model-incompatible levels.
76
79
 
77
80
  ## Issues and feedback
78
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hk_net/pi-thinking-command",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Pi extension that adds a /thinking slash command for changing reasoning level",
5
5
  "type": "module",
6
6
  "private": false,
@@ -23,11 +23,27 @@
23
23
  "homepage": "https://github.com/hknet/pi-extensions/tree/main/packages/pi-thinking-command#readme",
24
24
  "files": [
25
25
  "thinking-shortcut.ts",
26
- "README.md"
26
+ "README.md",
27
+ "CHANGELOG.md"
27
28
  ],
29
+ "engines": {
30
+ "node": ">=22.19.0"
31
+ },
28
32
  "peerDependencies": {
29
- "@earendil-works/pi-coding-agent": ">=0.84.1",
30
- "@earendil-works/pi-tui": ">=0.84.1"
33
+ "@earendil-works/pi-ai": "*",
34
+ "@earendil-works/pi-coding-agent": "*",
35
+ "@earendil-works/pi-tui": "*"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "@earendil-works/pi-ai": {
39
+ "optional": true
40
+ },
41
+ "@earendil-works/pi-coding-agent": {
42
+ "optional": true
43
+ },
44
+ "@earendil-works/pi-tui": {
45
+ "optional": true
46
+ }
31
47
  },
32
48
  "pi": {
33
49
  "extensions": [
@@ -1,155 +1,150 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
-
4
- type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
5
-
6
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
7
-
8
- const THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
9
- off: "Disable extended thinking",
10
- minimal: "Use the smallest available thinking budget",
11
- low: "Light reasoning",
12
- medium: "Balanced default reasoning",
13
- high: "More reasoning for harder tasks",
14
- xhigh: "Very high reasoning budget",
15
- max: "Maximum reasoning budget",
16
- };
17
-
18
- export function normalizeThinkingLevel(input: string): ThinkingLevel | undefined {
19
- const normalized = input.toLowerCase().trim();
20
- return THINKING_LEVELS.find((level) => level === normalized);
21
- }
22
-
23
- export function getThinkingLevelCompletions(prefix: string): AutocompleteItem[] | null {
24
- const normalizedPrefix = prefix.toLowerCase().trimStart();
25
- const matches = THINKING_LEVELS.filter((level) => level.startsWith(normalizedPrefix));
26
-
27
- if (matches.length === 0) return null;
28
-
29
- return matches.map((level) => ({
30
- value: level,
31
- label: level,
32
- description: THINKING_LEVEL_DESCRIPTIONS[level],
33
- }));
34
- }
35
-
36
- export function isThinkingCommandPrefix(value: string): boolean {
37
- return value.startsWith("/th") && "/thinking".startsWith(value);
38
- }
39
-
40
- export default function thinkingShortcutExtension(pi: ExtensionAPI) {
41
- pi.on("session_start", (_event, ctx) => {
42
- ctx.ui.addAutocompleteProvider((current) => ({
43
- async getSuggestions(lines, cursorLine, cursorCol, options) {
44
- const line = lines[cursorLine] ?? "";
45
- const beforeCursor = line.slice(0, cursorCol);
46
- if (cursorCol === line.length && isThinkingCommandPrefix(beforeCursor)) {
47
- if (beforeCursor === "/thinking") {
48
- return { prefix: "", items: getThinkingLevelCompletions("") ?? [] };
49
- }
50
- return {
51
- prefix: beforeCursor,
52
- items: [{ value: "thinking", label: "thinking", description: "Set the thinking level" }],
53
- };
54
- }
55
- if (beforeCursor === "/thinking ") {
56
- return { prefix: "", items: getThinkingLevelCompletions("") ?? [] };
57
- }
58
- return current.getSuggestions(lines, cursorLine, cursorCol, options);
59
- },
60
-
61
- applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
62
- const line = lines[cursorLine] ?? "";
63
- const beforeCursor = line.slice(0, cursorCol);
64
- if (cursorCol === line.length && isThinkingCommandPrefix(beforeCursor)) {
65
- if (beforeCursor !== "/thinking") {
66
- return {
67
- lines: [...lines.slice(0, cursorLine), "/thinking", ...lines.slice(cursorLine + 1)],
68
- cursorLine,
69
- cursorCol: "/thinking".length,
70
- };
71
- }
72
- const nextLine = `/thinking ${item.value}`;
73
- return {
74
- lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
75
- cursorLine,
76
- cursorCol: nextLine.length,
77
- };
78
- }
79
- if (beforeCursor === "/thinking " && cursorCol === line.length) {
80
- const nextLine = `/thinking ${item.value}`;
81
- return {
82
- lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
83
- cursorLine,
84
- cursorCol: nextLine.length,
85
- };
86
- }
87
- return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
88
- },
89
-
90
- shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
91
- const line = lines[cursorLine] ?? "";
92
- const beforeCursor = line.slice(0, cursorCol);
93
- if (
94
- (isThinkingCommandPrefix(beforeCursor) || beforeCursor === "/thinking ") &&
95
- cursorCol === line.length
96
- ) return false;
97
- return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
98
- },
99
- }));
100
-
101
- ctx.ui.setStatus("thinking", `level: ${pi.getThinkingLevel()}`);
102
-
103
- ctx.ui.addAutocompleteProvider((current) => ({
104
- async getSuggestions(lines, cursorLine, cursorCol, options) {
105
- const line = lines[cursorLine] ?? "";
106
- const beforeCursor = line.slice(0, cursorCol);
107
- const match = beforeCursor.match(/^\/thinking\s+(\S*)$/);
108
-
109
- if (!match) {
110
- return current.getSuggestions(lines, cursorLine, cursorCol, options);
111
- }
112
-
113
- const prefix = match[1] ?? "";
114
- const items = getThinkingLevelCompletions(prefix);
115
- return items ? { prefix, items } : null;
116
- },
117
-
118
- applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
119
- return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
120
- },
121
-
122
- shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
123
- const line = lines[cursorLine] ?? "";
124
- const beforeCursor = line.slice(0, cursorCol);
125
- if (/^\/thinking\s+\S*$/.test(beforeCursor)) return false;
126
- return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
127
- },
128
- }));
129
- });
130
-
131
- pi.on("thinking_level_select", (event, ctx) => {
132
- ctx.ui.setStatus("thinking", `level: ${event.level}`);
133
- });
134
-
135
- pi.registerCommand("thinking", {
136
- description: "Set the thinking level. Usage: /thinking [off|minimal|low|medium|high|xhigh|max]",
137
- getArgumentCompletions: getThinkingLevelCompletions,
138
- handler: async (args, ctx) => {
139
- const input = args?.trim() || "medium";
140
- const level = normalizeThinkingLevel(input);
141
-
142
- if (!level) {
143
- ctx.ui.notify(
144
- `Invalid level: "${input}". Valid options are: ${THINKING_LEVELS.join(", ")}.`,
145
- "error",
146
- );
147
- return;
148
- }
149
-
150
- pi.setThinkingLevel(level);
151
-
152
- ctx.ui.notify(`Thinking level set to: ${pi.getThinkingLevel()}`, "info");
153
- },
154
- });
155
- }
1
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
+ import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
5
+
6
+ type ThinkingLevel = ModelThinkingLevel;
7
+
8
+ const THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
9
+ off: "Disable extended thinking",
10
+ minimal: "Use the smallest available thinking budget",
11
+ low: "Light reasoning",
12
+ medium: "Balanced default reasoning",
13
+ high: "More reasoning for harder tasks",
14
+ xhigh: "Very high reasoning budget",
15
+ max: "Maximum reasoning budget",
16
+ };
17
+
18
+ export function normalizeThinkingLevel(
19
+ input: string,
20
+ supportedLevels: readonly ThinkingLevel[],
21
+ ): ThinkingLevel | undefined {
22
+ const normalized = input.toLowerCase().trim();
23
+ return supportedLevels.find((level) => level === normalized);
24
+ }
25
+
26
+ export function getThinkingLevelCompletions(
27
+ prefix: string,
28
+ supportedLevels: readonly ThinkingLevel[],
29
+ ): AutocompleteItem[] | null {
30
+ const normalizedPrefix = prefix.toLowerCase().trimStart();
31
+ const matches = supportedLevels.filter((level) => level.startsWith(normalizedPrefix));
32
+
33
+ if (matches.length === 0) return null;
34
+
35
+ return matches.map((level) => ({
36
+ value: level,
37
+ label: level,
38
+ description: THINKING_LEVEL_DESCRIPTIONS[level],
39
+ }));
40
+ }
41
+
42
+ export function isThinkingCommandPrefix(value: string): boolean {
43
+ return value.startsWith("/th") && "/thinking".startsWith(value);
44
+ }
45
+
46
+ export default function thinkingShortcutExtension(pi: ExtensionAPI) {
47
+ let supportedLevels: ThinkingLevel[] = ["off"];
48
+
49
+ const updateSupportedLevels = (model: Model<Api> | undefined) => {
50
+ const resolved: ThinkingLevel[] = model ? getSupportedThinkingLevels(model) : ["off"];
51
+ supportedLevels = resolved.length > 0 ? resolved : ["off"];
52
+ };
53
+
54
+ pi.on("session_start", (_event, ctx) => {
55
+ updateSupportedLevels(ctx.model);
56
+ ctx.ui.addAutocompleteProvider((current) => ({
57
+ async getSuggestions(lines, cursorLine, cursorCol, options) {
58
+ const line = lines[cursorLine] ?? "";
59
+ const beforeCursor = line.slice(0, cursorCol);
60
+ if (cursorCol === line.length && isThinkingCommandPrefix(beforeCursor)) {
61
+ if (beforeCursor === "/thinking") {
62
+ return { prefix: "", items: getThinkingLevelCompletions("", supportedLevels) ?? [] };
63
+ }
64
+ return {
65
+ prefix: beforeCursor,
66
+ items: [{ value: "thinking", label: "thinking", description: "Set the thinking level" }],
67
+ };
68
+ }
69
+ if (beforeCursor === "/thinking " && cursorCol === line.length) {
70
+ return { prefix: "", items: getThinkingLevelCompletions("", supportedLevels) ?? [] };
71
+ }
72
+
73
+ const match = beforeCursor.match(/^\/thinking\s+(\S*)$/);
74
+ if (match && cursorCol === line.length) {
75
+ const prefix = match[1] ?? "";
76
+ const items = getThinkingLevelCompletions(prefix, supportedLevels);
77
+ return items ? { prefix, items } : null;
78
+ }
79
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
80
+ },
81
+
82
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
83
+ const line = lines[cursorLine] ?? "";
84
+ const beforeCursor = line.slice(0, cursorCol);
85
+ if (cursorCol === line.length && isThinkingCommandPrefix(beforeCursor)) {
86
+ if (beforeCursor !== "/thinking") {
87
+ return {
88
+ lines: [...lines.slice(0, cursorLine), "/thinking", ...lines.slice(cursorLine + 1)],
89
+ cursorLine,
90
+ cursorCol: "/thinking".length,
91
+ };
92
+ }
93
+ const nextLine = `/thinking ${item.value}`;
94
+ return {
95
+ lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
96
+ cursorLine,
97
+ cursorCol: nextLine.length,
98
+ };
99
+ }
100
+ if (beforeCursor === "/thinking " && cursorCol === line.length) {
101
+ const nextLine = `/thinking ${item.value}`;
102
+ return {
103
+ lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
104
+ cursorLine,
105
+ cursorCol: nextLine.length,
106
+ };
107
+ }
108
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
109
+ },
110
+
111
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
112
+ const line = lines[cursorLine] ?? "";
113
+ const beforeCursor = line.slice(0, cursorCol);
114
+ if (/^\/thinking(?:\s+\S*)?$/.test(beforeCursor) && cursorCol === line.length) return false;
115
+ return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
116
+ },
117
+ }));
118
+
119
+ ctx.ui.setStatus("thinking", `level: ${pi.getThinkingLevel()}`);
120
+ });
121
+
122
+ pi.on("model_select", (event) => {
123
+ updateSupportedLevels(event.model);
124
+ });
125
+
126
+ pi.on("thinking_level_select", (event, ctx) => {
127
+ ctx.ui.setStatus("thinking", `level: ${event.level}`);
128
+ });
129
+
130
+ pi.registerCommand("thinking", {
131
+ description: "Set the thinking level supported by the active model",
132
+ getArgumentCompletions: (prefix) => getThinkingLevelCompletions(prefix, supportedLevels),
133
+ handler: async (args, ctx) => {
134
+ updateSupportedLevels(ctx.model);
135
+ const input = args?.trim() || (supportedLevels.includes("medium") ? "medium" : supportedLevels[0] ?? "off");
136
+ const level = normalizeThinkingLevel(input, supportedLevels);
137
+
138
+ if (!level) {
139
+ ctx.ui.notify(
140
+ `Thinking level "${input}" is not supported by the active model. Supported: ${supportedLevels.join(", ")}.`,
141
+ "error",
142
+ );
143
+ return;
144
+ }
145
+
146
+ pi.setThinkingLevel(level);
147
+ ctx.ui.notify(`Thinking level set to: ${pi.getThinkingLevel()}`, "info");
148
+ },
149
+ });
150
+ }