pi-spark 0.1.1 → 0.2.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 +4 -3
- package/extensions/editor/index.ts +63 -4
- package/extensions/editor/spinner.ts +11 -6
- package/extensions/footer/index.ts +17 -25
- package/extensions/presets/index.ts +16 -2
- package/extensions/presets/manager.ts +1 -1
- package/extensions/recap/manager.ts +19 -16
- package/extensions/recap/model.ts +61 -27
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ A small, opinionated collection of [pi](https://pi.dev/) extensions.
|
|
|
6
6
|
|
|
7
7
|
## Extensions
|
|
8
8
|
|
|
9
|
-
- **Editor**: replaces the default editor with a compact working indicator and current model info.
|
|
9
|
+
- **Editor**: replaces the default editor with a compact working indicator (inspired by [Amp](https://ampcode.com/)) and current model info.
|
|
10
10
|
- **Footer**: shows the session info, cost, and context usage in one line, followed by extension statuses.
|
|
11
11
|
- **Fullscreen**: clears the screen and scrollback on session start and pins the editor and footer to the bottom for a full-screen session.
|
|
12
|
-
- **Presets**: switches named model presets with `/preset
|
|
12
|
+
- **Presets**: switches named model presets with `/preset`, `--preset`, and quick cycle shortcuts.
|
|
13
13
|
- **Recap**: generates a short idle-session recap and exposes a `/recap` command for manual generation, inspired by [Claude Code's session recap](https://code.claude.com/docs/en/interactive-mode#session-recap).
|
|
14
14
|
|
|
15
15
|

|
|
@@ -64,8 +64,9 @@ Example:
|
|
|
64
64
|
Notes:
|
|
65
65
|
|
|
66
66
|
- Set an extension key to `false` to disable it.
|
|
67
|
-
- The `editor.spinner` value can be `lights
|
|
67
|
+
- The `editor.spinner` value can be `dots`, `lights`, or `tildes`.
|
|
68
68
|
- Presets can be selected with `/preset` or `/preset <key>`.
|
|
69
|
+
- Start pi with a preset using `pi --preset <key>`.
|
|
69
70
|
- Cycle presets with `ctrl+super+p` and `ctrl+shift+super+p` (`super` is `command` on macOS).
|
|
70
71
|
- The `recap.idle` value is in milliseconds and must be at least `5000`.
|
|
71
72
|
|
|
@@ -14,6 +14,7 @@ class Editor extends CustomEditor {
|
|
|
14
14
|
private ctx: ExtensionContext;
|
|
15
15
|
|
|
16
16
|
private spinner: Spinner;
|
|
17
|
+
private workingMessage: string | undefined;
|
|
17
18
|
private slots: { modelBefore: string | undefined };
|
|
18
19
|
|
|
19
20
|
constructor(pi: ExtensionAPI, ctx: ExtensionContext, tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, spinner: Spinner = new Spinner()) {
|
|
@@ -24,10 +25,16 @@ class Editor extends CustomEditor {
|
|
|
24
25
|
|
|
25
26
|
this.spinner = spinner;
|
|
26
27
|
this.spinner.setTUI(tui);
|
|
28
|
+
this.workingMessage = undefined;
|
|
27
29
|
this.slots = { modelBefore: undefined };
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
setWorkingMessage(message?: string | undefined): void {
|
|
33
|
+
this.workingMessage = message;
|
|
34
|
+
this.tui.requestRender();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setSlot(slot: keyof typeof this.slots, value?: string | undefined): void {
|
|
31
38
|
this.slots[slot] = value;
|
|
32
39
|
this.tui.requestRender();
|
|
33
40
|
}
|
|
@@ -58,7 +65,10 @@ class Editor extends CustomEditor {
|
|
|
58
65
|
private getLeft(): string {
|
|
59
66
|
const theme = this.ctx.ui.theme;
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
const spinner = this.spinner.getFrame();
|
|
69
|
+
const workingMessage = this.workingMessage;
|
|
70
|
+
|
|
71
|
+
return [theme.fg("accent", spinner), workingMessage ? theme.fg("dim", workingMessage) : undefined].filter(Boolean).join(" ");
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
private getRight(): string {
|
|
@@ -73,7 +83,10 @@ class Editor extends CustomEditor {
|
|
|
73
83
|
|
|
74
84
|
export default function (pi: ExtensionAPI) {
|
|
75
85
|
const events = autoCollectEvents(pi);
|
|
86
|
+
|
|
87
|
+
let editor: Editor | undefined = undefined;
|
|
76
88
|
let spinner: Spinner | undefined = undefined;
|
|
89
|
+
let runningToolCallIds = new Set<string>();
|
|
77
90
|
|
|
78
91
|
pi.on("session_start", (_event, ctx) => {
|
|
79
92
|
if (!ctx.hasUI) return;
|
|
@@ -85,11 +98,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
85
98
|
|
|
86
99
|
ctx.ui.setWorkingVisible(false);
|
|
87
100
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
88
|
-
|
|
101
|
+
editor = new Editor(pi, ctx, tui, theme, keybindings, spinner);
|
|
89
102
|
|
|
90
103
|
events.on(PRESET_CHANGE, (data) => {
|
|
91
104
|
const payload = parsePresetChange(data);
|
|
92
|
-
editor
|
|
105
|
+
editor?.setSlot("modelBefore", payload ? `preset:${payload}` : undefined);
|
|
93
106
|
});
|
|
94
107
|
|
|
95
108
|
return editor;
|
|
@@ -97,14 +110,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
97
110
|
});
|
|
98
111
|
|
|
99
112
|
pi.on("agent_start", () => {
|
|
113
|
+
runningToolCallIds.clear();
|
|
114
|
+
editor?.setWorkingMessage();
|
|
100
115
|
spinner?.start();
|
|
101
116
|
});
|
|
102
117
|
|
|
118
|
+
pi.on("message_update", (event) => {
|
|
119
|
+
if (runningToolCallIds.size > 0) return;
|
|
120
|
+
|
|
121
|
+
switch (event.assistantMessageEvent.type) {
|
|
122
|
+
case "thinking_start":
|
|
123
|
+
case "thinking_delta":
|
|
124
|
+
case "thinking_end":
|
|
125
|
+
editor?.setWorkingMessage("Thinking");
|
|
126
|
+
break;
|
|
127
|
+
case "text_start":
|
|
128
|
+
case "text_delta":
|
|
129
|
+
case "text_end":
|
|
130
|
+
editor?.setWorkingMessage("Streaming");
|
|
131
|
+
break;
|
|
132
|
+
case "toolcall_start":
|
|
133
|
+
case "toolcall_delta":
|
|
134
|
+
case "toolcall_end":
|
|
135
|
+
editor?.setWorkingMessage("Running tools");
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
editor?.setWorkingMessage();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.on("tool_execution_start", (event) => {
|
|
144
|
+
runningToolCallIds.add(event.toolCallId);
|
|
145
|
+
editor?.setWorkingMessage("Running tools");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.on("tool_call", (event) => {
|
|
149
|
+
runningToolCallIds.add(event.toolCallId);
|
|
150
|
+
editor?.setWorkingMessage("Running tools");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.on("tool_execution_end", (event) => {
|
|
154
|
+
runningToolCallIds.delete(event.toolCallId);
|
|
155
|
+
editor?.setWorkingMessage(runningToolCallIds.size > 0 ? "Running tools" : undefined);
|
|
156
|
+
});
|
|
157
|
+
|
|
103
158
|
pi.on("agent_end", () => {
|
|
159
|
+
runningToolCallIds.clear();
|
|
160
|
+
editor?.setWorkingMessage();
|
|
104
161
|
spinner?.stop();
|
|
105
162
|
});
|
|
106
163
|
|
|
107
164
|
pi.on("session_shutdown", () => {
|
|
165
|
+
runningToolCallIds.clear();
|
|
166
|
+
editor = undefined;
|
|
108
167
|
spinner?.dispose();
|
|
109
168
|
spinner = undefined;
|
|
110
169
|
});
|
|
@@ -2,7 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
|
|
3
3
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
|
|
5
|
-
export const spinnerPresetSchema = z.enum(["lights", "
|
|
5
|
+
export const spinnerPresetSchema = z.enum(["dots", "lights", "tildes"]);
|
|
6
6
|
|
|
7
7
|
type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
|
|
8
8
|
|
|
@@ -13,19 +13,24 @@ interface SpinnerParams {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const SPINNER_PRESETS: Record<SpinnerPreset, SpinnerParams> = {
|
|
16
|
+
dots: {
|
|
17
|
+
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
18
|
+
interval: 80,
|
|
19
|
+
random: false,
|
|
20
|
+
},
|
|
16
21
|
lights: {
|
|
17
|
-
frames: ["
|
|
22
|
+
frames: ["○", "●"],
|
|
18
23
|
interval: { min: 120, max: 240 },
|
|
19
24
|
random: true,
|
|
20
25
|
},
|
|
21
|
-
|
|
22
|
-
frames: ["
|
|
23
|
-
interval:
|
|
26
|
+
tildes: {
|
|
27
|
+
frames: ["∼", "≈", "≋", "≈", "∼"],
|
|
28
|
+
interval: 200,
|
|
24
29
|
random: false,
|
|
25
30
|
},
|
|
26
31
|
};
|
|
27
32
|
|
|
28
|
-
const DEFAULT_SPINNER_PRESET = "
|
|
33
|
+
const DEFAULT_SPINNER_PRESET = "tildes";
|
|
29
34
|
|
|
30
35
|
export class Spinner {
|
|
31
36
|
private tui: TUI | undefined;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
2
|
|
|
4
3
|
import { SplitLine } from "../shared/components/split-line";
|
|
5
4
|
import { loadConfig } from "../shared/config";
|
|
@@ -25,16 +24,10 @@ class FooterComponent implements Component {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
render(width: number): string[] {
|
|
28
|
-
return [this.renderMainLine(width), this.renderStatusLine(width)].filter(Boolean);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private renderMainLine(width: number): string {
|
|
32
27
|
const left = this.getLeft();
|
|
33
28
|
const right = this.getRight();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
ellipsis: this.theme.fg("dim", "…"),
|
|
37
|
-
}).render(width)[0];
|
|
29
|
+
|
|
30
|
+
return new SplitLine(left, right, { primarySide: "right", ellipsis: this.theme.fg("dim", "…") }).render(width);
|
|
38
31
|
}
|
|
39
32
|
|
|
40
33
|
private getLeft(): string {
|
|
@@ -50,10 +43,22 @@ class FooterComponent implements Component {
|
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
private getRight(): string {
|
|
46
|
+
const statusesText = this.getStatusesText();
|
|
53
47
|
const styledCostText = this.getStyledCostText();
|
|
54
48
|
const styledContextUsageText = this.getStyledContextUsageText();
|
|
55
49
|
|
|
56
|
-
return
|
|
50
|
+
return [statusesText, styledCostText, styledContextUsageText].filter(Boolean).join(this.theme.fg("dim", " • "));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Get extension statuses, sorted by key alphabetically. */
|
|
54
|
+
private getStatusesText(): string {
|
|
55
|
+
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
56
|
+
if (extensionStatuses.size === 0) return "";
|
|
57
|
+
|
|
58
|
+
return Array.from(extensionStatuses.entries())
|
|
59
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
60
|
+
.map(([, text]) => sanitizeText(text))
|
|
61
|
+
.join(this.theme.fg("dim", " • "));
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
private getStyledCostText(): string {
|
|
@@ -64,8 +69,8 @@ class FooterComponent implements Component {
|
|
|
64
69
|
}, { subscription: 0, paid: 0 });
|
|
65
70
|
|
|
66
71
|
const isSubscription = this.ctx.model ? this.ctx.modelRegistry.isUsingOAuth(this.ctx.model) : false;
|
|
67
|
-
const subscriptionCostText = isSubscription || cost.subscription
|
|
68
|
-
const paidCostText = !isSubscription || cost.paid
|
|
72
|
+
const subscriptionCostText = isSubscription || cost.subscription >= 0.005 ? formatCost(cost.subscription, true) : undefined;
|
|
73
|
+
const paidCostText = !isSubscription || cost.paid >= 0.005 ? formatCost(cost.paid, false) : undefined;
|
|
69
74
|
const costText = [subscriptionCostText, paidCostText].filter(Boolean).join(" + ");
|
|
70
75
|
|
|
71
76
|
const totalCost = cost.subscription + cost.paid;
|
|
@@ -83,19 +88,6 @@ class FooterComponent implements Component {
|
|
|
83
88
|
if (percent && percent > 70) return this.theme.fg("warning", contextUsageText);
|
|
84
89
|
return this.theme.fg("dim", contextUsageText);
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
/** Add extension statues on a single line, sorted by key alphabetically. */
|
|
88
|
-
private renderStatusLine(width: number): string {
|
|
89
|
-
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
90
|
-
if (extensionStatuses.size === 0) return "";
|
|
91
|
-
|
|
92
|
-
const statusLine = Array.from(extensionStatuses.entries())
|
|
93
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
94
|
-
.map(([, text]) => sanitizeText(text))
|
|
95
|
-
.join(this.theme.fg("dim", " • "));
|
|
96
|
-
|
|
97
|
-
return truncateToWidth(statusLine, width, this.theme.fg("dim", "..."));
|
|
98
|
-
}
|
|
99
91
|
}
|
|
100
92
|
|
|
101
93
|
function formatCwd(cwd: string, home?: string): string {
|
|
@@ -9,9 +9,19 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
9
9
|
export default function (pi: ExtensionAPI) {
|
|
10
10
|
let presetManager: PresetManager | undefined = undefined;
|
|
11
11
|
|
|
12
|
-
pi.
|
|
12
|
+
pi.registerFlag("preset", {
|
|
13
|
+
description: "Model preset to use",
|
|
14
|
+
type: "string",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
pi.on("session_start", async (event, ctx) => {
|
|
18
|
+
const presetFlag = pi.getFlag("preset");
|
|
19
|
+
|
|
13
20
|
const config = loadConfig(ctx, "presets");
|
|
14
|
-
if (!config || Object.keys(config).length === 0)
|
|
21
|
+
if (!config || Object.keys(config).length === 0) {
|
|
22
|
+
if (presetFlag) ctx.ui.notify("No presets defined in spark.json", "warning");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
presetManager = new PresetManager(pi, config);
|
|
17
27
|
presetManager.sync(ctx);
|
|
@@ -42,6 +52,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
42
52
|
}
|
|
43
53
|
},
|
|
44
54
|
});
|
|
55
|
+
|
|
56
|
+
if (presetFlag && typeof presetFlag === "string") {
|
|
57
|
+
await presetManager.apply(presetFlag, ctx);
|
|
58
|
+
}
|
|
45
59
|
});
|
|
46
60
|
|
|
47
61
|
pi.on("model_select", (_event, ctx) => {
|
|
@@ -35,7 +35,7 @@ export class PresetManager {
|
|
|
35
35
|
async apply(key: string, ctx: ExtensionContext): Promise<boolean> {
|
|
36
36
|
const preset = this.presets[key];
|
|
37
37
|
if (!preset) {
|
|
38
|
-
ctx.ui.notify(`Unknown preset ${key}`, "error");
|
|
38
|
+
ctx.ui.notify(`Unknown preset ${key} (${this.keys.length ? `available: ${this.keys.join(", ")}` : "none defined"})`, "error");
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { completeSimple } from "@earendil-works/pi-ai";
|
|
2
2
|
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { resolveRecapModelSettings } from "./model";
|
|
5
5
|
import { clearRecapWidget, setRecapLoadingWidget, setRecapTextWidget } from "./widget";
|
|
6
6
|
import { sanitizeText } from "../shared/format";
|
|
7
7
|
|
|
@@ -10,14 +10,15 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
10
10
|
import type { RecapConfig } from "./config";
|
|
11
11
|
|
|
12
12
|
const SYSTEM_PROMPT = [
|
|
13
|
-
"You write concise recaps for
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
13
|
+
"You write concise idle-session recaps for a terminal coding agent.",
|
|
14
|
+
"Use only transcript-supported facts; do not invent progress, intent, files, or next steps.",
|
|
15
|
+
"Prefer the latest active task if the session changed direction.",
|
|
16
|
+
"Summarize the user's goal, what was done, current state, and any clearly supported next step.",
|
|
17
|
+
"Respond in the conversation's primary language.",
|
|
18
|
+
"Output 1-2 plain-text sentences under 80 words. No heading, markdown, bullets, or quotes.",
|
|
18
19
|
].join("\n");
|
|
19
20
|
|
|
20
|
-
const MAX_TOKENS =
|
|
21
|
+
const MAX_TOKENS = 100;
|
|
21
22
|
const MAX_CONVERSATION_CHARS = 8_000;
|
|
22
23
|
|
|
23
24
|
export class RecapManager {
|
|
@@ -39,25 +40,27 @@ export class RecapManager {
|
|
|
39
40
|
this.inflight = controller;
|
|
40
41
|
|
|
41
42
|
try {
|
|
42
|
-
const
|
|
43
|
-
if (controller.signal.aborted || this.inflight !== controller || !
|
|
43
|
+
const recapModelSettings = await resolveRecapModelSettings(this.pi, ctx, this.config);
|
|
44
|
+
if (controller.signal.aborted || this.inflight !== controller || !recapModelSettings) return;
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
const { model, thinkingLevel, warning } = recapModelSettings;
|
|
47
|
+
|
|
48
|
+
setRecapLoadingWidget(ctx, warning);
|
|
46
49
|
this.active = false;
|
|
47
50
|
|
|
48
|
-
const result = await this.generate(ctx,
|
|
51
|
+
const result = await this.generate(ctx, model, thinkingLevel, controller.signal);
|
|
49
52
|
if (controller.signal.aborted || this.inflight !== controller) return;
|
|
50
53
|
if (!result.content) {
|
|
51
54
|
clearRecapWidget(ctx);
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
setRecapTextWidget(ctx, result.content,
|
|
58
|
+
setRecapTextWidget(ctx, result.content, warning);
|
|
56
59
|
this.active = true;
|
|
57
60
|
|
|
58
61
|
this.pi.appendEntry("recap", {
|
|
59
|
-
provider:
|
|
60
|
-
model:
|
|
62
|
+
provider: model.provider,
|
|
63
|
+
model: model.id,
|
|
61
64
|
usage: result.usage,
|
|
62
65
|
content: result.content,
|
|
63
66
|
});
|
|
@@ -111,8 +114,8 @@ export class RecapManager {
|
|
|
111
114
|
const conversation = text.length > MAX_CONVERSATION_CHARS ? text.slice(-MAX_CONVERSATION_CHARS) : text;
|
|
112
115
|
|
|
113
116
|
return [
|
|
114
|
-
"Create a short recap
|
|
115
|
-
"
|
|
117
|
+
"Create a short recap from this transcript, ordered oldest to newest.",
|
|
118
|
+
"It may be truncated from the beginning; focus on the latest coherent task.",
|
|
116
119
|
"",
|
|
117
120
|
"<conversation>",
|
|
118
121
|
conversation,
|
|
@@ -1,61 +1,95 @@
|
|
|
1
1
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
|
+
import { formatModel } from "../shared/format";
|
|
4
|
+
|
|
3
5
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
7
|
import type { OptionalModelConfig } from "../shared/config/model";
|
|
6
8
|
|
|
7
9
|
const DEFAULT_THINKING_LEVEL: ModelThinkingLevel = "off";
|
|
8
10
|
|
|
9
|
-
type
|
|
11
|
+
type RecapModelSettings = {
|
|
10
12
|
model: Model<Api>;
|
|
11
13
|
thinkingLevel: ModelThinkingLevel;
|
|
12
14
|
warning: string | undefined;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
type ModelSelection = {
|
|
18
|
+
model: Model<Api>;
|
|
19
|
+
warning: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ThinkingLevelSelection = {
|
|
23
|
+
thinkingLevel: ModelThinkingLevel;
|
|
24
|
+
warning: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function resolveRecapModelSettings(pi: ExtensionAPI, ctx: ExtensionContext, config: OptionalModelConfig): Promise<RecapModelSettings | undefined> {
|
|
16
28
|
const fallbackModel = ctx.model;
|
|
17
29
|
if (!fallbackModel) {
|
|
18
30
|
ctx.ui.notify("No model selected for recap", "warning");
|
|
19
31
|
return;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
let warning: string | undefined;
|
|
24
|
-
|
|
25
|
-
if (config.provider || config.model) {
|
|
26
|
-
if (!config.provider || !config.model) {
|
|
27
|
-
warning = "Both recap.provider and recap.model are required; using the current model.";
|
|
28
|
-
} else {
|
|
29
|
-
const configuredModel = ctx.modelRegistry.find(config.provider, config.model);
|
|
30
|
-
if (configuredModel) {
|
|
31
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configuredModel);
|
|
32
|
-
if (auth.ok) {
|
|
33
|
-
model = configuredModel;
|
|
34
|
-
} else {
|
|
35
|
-
warning = `Model ${config.provider}/${config.model} unavailable: ${auth.error}; using the current model.`;
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
warning = `Model ${config.provider}/${config.model} not found; using the current model.`;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
34
|
+
const { model, warning: modelWarning } = await resolveModelSelection(ctx, config, fallbackModel);
|
|
43
35
|
const { thinkingLevel, warning: thinkingLevelWarning } = resolveThinkingLevel(model, config.thinkingLevel ?? pi.getThinkingLevel());
|
|
44
36
|
|
|
45
37
|
return {
|
|
46
38
|
model,
|
|
47
39
|
thinkingLevel,
|
|
48
|
-
warning: [
|
|
40
|
+
warning: [modelWarning, thinkingLevelWarning].filter(Boolean).join(" ") || undefined,
|
|
49
41
|
};
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
function
|
|
44
|
+
async function resolveModelSelection(ctx: ExtensionContext, config: OptionalModelConfig, fallbackModel: Model<Api>): Promise<ModelSelection> {
|
|
45
|
+
if (!config.provider && !config.model) {
|
|
46
|
+
return {
|
|
47
|
+
model: fallbackModel,
|
|
48
|
+
warning: undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!config.provider || !config.model) {
|
|
53
|
+
return {
|
|
54
|
+
model: fallbackModel,
|
|
55
|
+
warning: "Both recap.provider and recap.model are required; using the current model.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const model = ctx.modelRegistry.find(config.provider, config.model);
|
|
60
|
+
if (!model) {
|
|
61
|
+
return {
|
|
62
|
+
model: fallbackModel,
|
|
63
|
+
warning: `Model ${formatModel(config.provider, config.model)} not found; using the current model.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
68
|
+
if (!auth.ok) {
|
|
69
|
+
return {
|
|
70
|
+
model: fallbackModel,
|
|
71
|
+
warning: `Model ${formatModel(config.provider, config.model)} unavailable: ${auth.error}; using the current model.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
model,
|
|
77
|
+
warning: undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveThinkingLevel(model: Model<Api>, requested: ModelThinkingLevel): ThinkingLevelSelection {
|
|
53
82
|
const thinkingLevel = clampThinkingLevel(model, requested);
|
|
54
|
-
if (thinkingLevel === requested)
|
|
83
|
+
if (thinkingLevel === requested) {
|
|
84
|
+
return {
|
|
85
|
+
thinkingLevel,
|
|
86
|
+
warning: undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
55
89
|
|
|
56
90
|
const fallback = clampThinkingLevel(model, DEFAULT_THINKING_LEVEL);
|
|
57
91
|
return {
|
|
58
92
|
thinkingLevel: fallback,
|
|
59
|
-
warning: `Thinking level ${requested} is not supported by ${model.provider
|
|
93
|
+
warning: `Thinking level ${requested} is not supported by ${formatModel(model.provider, model.id)}; using ${fallback}.`,
|
|
60
94
|
};
|
|
61
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A small, opinionated collection of pi extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"pi": {
|
|
22
|
-
"extensions": [
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./extensions"
|
|
24
|
+
],
|
|
23
25
|
"image": "https://raw.githubusercontent.com/zlliang/pi-spark/main/assets/cover.png"
|
|
24
26
|
},
|
|
25
27
|
"scripts": {
|