@pithos-kit/squiggle 0.5.0 → 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 +18 -6
- package/extensions/index.ts +204 -34
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -39,33 +39,40 @@ pi -e ./pithos.squiggle
|
|
|
39
39
|
```yaml
|
|
40
40
|
pi:
|
|
41
41
|
extensions:
|
|
42
|
-
"@pithos-kit/squiggle": "npm:0.
|
|
42
|
+
"@pithos-kit/squiggle": "npm:0.6.0"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Configuration
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Run `/squiggle config` to choose an exact correction model from providers with configured authentication, just like Translate's model picker. If none are available, run `/login` first. The picker opens directly and saves to `<cwd>/.pi/squiggle.json` (honors Pi's configured project directory name). There is no session or user-level model configuration.
|
|
48
|
+
|
|
49
|
+
Model precedence is **`SQUIGGLE_MODEL` > project > default**. An environment override may still take precedence after saving, which the completion message shows. Cancelling the picker changes nothing. Existing file fields are preserved, and malformed files are not overwritten.
|
|
50
|
+
|
|
51
|
+
You can still edit `.pi/squiggle.json` directly:
|
|
48
52
|
|
|
49
53
|
```json
|
|
50
54
|
{
|
|
51
55
|
"mode": "on",
|
|
52
56
|
"model": "openai-codex/gpt-5.4-mini",
|
|
53
|
-
"maxInputChars": 500
|
|
57
|
+
"maxInputChars": 500,
|
|
58
|
+
"timeoutMs": 10000
|
|
54
59
|
}
|
|
55
60
|
```
|
|
56
61
|
|
|
57
62
|
Options:
|
|
58
63
|
|
|
59
64
|
- `mode`: `"on"` or `"off"`
|
|
60
|
-
- `model`: pi model spec in `provider/model` format
|
|
65
|
+
- `model`: exact pi model spec in `provider/model` format (model IDs may contain additional slashes)
|
|
61
66
|
- `maxInputChars`: maximum input length to send to the correction model
|
|
67
|
+
- `timeoutMs`: correction deadline in milliseconds, from `1000` to `60000` (default: `10000`)
|
|
62
68
|
|
|
63
|
-
Environment variables override the config
|
|
69
|
+
Environment variables override the project config (except the session on/off toggle):
|
|
64
70
|
|
|
65
71
|
```bash
|
|
66
72
|
SQUIGGLE_MODE=off pi
|
|
67
73
|
SQUIGGLE_MODEL=openai-codex/gpt-5.4-mini pi
|
|
68
74
|
SQUIGGLE_MAX_CHARS=1000 pi
|
|
75
|
+
SQUIGGLE_TIMEOUT_MS=15000 pi
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
## Commands
|
|
@@ -74,7 +81,8 @@ Inside pi:
|
|
|
74
81
|
|
|
75
82
|
```text
|
|
76
83
|
/squiggle toggle # switch between on/off
|
|
77
|
-
/squiggle
|
|
84
|
+
/squiggle config # choose and save the correction model
|
|
85
|
+
/squiggle --help # show toggle/config usage
|
|
78
86
|
/squiggle-status # show status
|
|
79
87
|
/squiggle-status --help # show status-command usage
|
|
80
88
|
```
|
|
@@ -83,6 +91,10 @@ Inside pi:
|
|
|
83
91
|
|
|
84
92
|
The toggle state is saved in the current pi session and overrides `.pi/squiggle.json` and environment configuration for that session.
|
|
85
93
|
|
|
94
|
+
The default correction model is `openai-codex/gpt-5.4-mini`. Squiggle uses only the exact configured model, never silently substituting the active coding model. `/squiggle-status` shows its exact ID, configuration source, and whether it is missing from the registry. Missing models or failed authentication leave the original prompt unchanged.
|
|
95
|
+
|
|
96
|
+
If authentication or correction exceeds the configured deadline, Squiggle cancels the request, clears the spinner, and submits the original prompt unchanged. Session shutdown, reload, and an active Pi cancellation signal also cancel in-flight correction.
|
|
97
|
+
|
|
86
98
|
## Notes
|
|
87
99
|
|
|
88
100
|
This package imports pi runtime packages as peer dependencies:
|
package/extensions/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { complete, type UserMessage } from "@earendil-works/pi-ai";
|
|
5
|
+
import * as piRuntime from "@earendil-works/pi-coding-agent";
|
|
4
6
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
7
|
import { createPithosLogger, errorMetadata, modelMetadata, usageMetadata, type PithosLogger } from "./logging.ts";
|
|
6
8
|
|
|
9
|
+
// Older supported Pi releases do not export CONFIG_DIR_NAME.
|
|
10
|
+
const CONFIG_DIR_NAME = (piRuntime as { CONFIG_DIR_NAME?: string }).CONFIG_DIR_NAME ?? ".pi";
|
|
11
|
+
|
|
7
12
|
const SQUIGGLE_HELP = `Usage: /squiggle toggle
|
|
13
|
+
/squiggle config
|
|
8
14
|
|
|
9
15
|
Toggle Squiggle on or off for the current session.
|
|
16
|
+
Configure an authenticated exact correction model for the project.
|
|
17
|
+
Model precedence: SQUIGGLE_MODEL > project > default.
|
|
18
|
+
Cancelling configuration makes no changes.
|
|
10
19
|
|
|
11
20
|
Options:
|
|
12
21
|
--help, -h Show this help`;
|
|
@@ -18,35 +27,83 @@ Show whether Squiggle is enabled and which correction model it uses.
|
|
|
18
27
|
Options:
|
|
19
28
|
--help, -h Show this help`;
|
|
20
29
|
|
|
30
|
+
export type SquiggleConfig = {
|
|
31
|
+
mode: "on" | "off";
|
|
32
|
+
model: string;
|
|
33
|
+
modelScope?: "SQUIGGLE_MODEL" | "project" | "default";
|
|
34
|
+
maxInputChars: number;
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_CORRECTION_TIMEOUT_MS = 10_000;
|
|
39
|
+
export const MIN_CORRECTION_TIMEOUT_MS = 1_000;
|
|
40
|
+
export const MAX_CORRECTION_TIMEOUT_MS = 60_000;
|
|
41
|
+
|
|
42
|
+
type CorrectPrompt = (
|
|
43
|
+
input: string,
|
|
44
|
+
ctx: ExtensionContext,
|
|
45
|
+
config: SquiggleConfig,
|
|
46
|
+
log?: PithosLogger,
|
|
47
|
+
signal?: AbortSignal,
|
|
48
|
+
) => Promise<string | null>;
|
|
49
|
+
|
|
21
50
|
export function registerSquiggle(
|
|
22
51
|
pi: ExtensionAPI,
|
|
23
|
-
correctPrompt:
|
|
52
|
+
correctPrompt: CorrectPrompt = correctWithModel,
|
|
24
53
|
) {
|
|
25
54
|
const log = createPithosLogger();
|
|
26
55
|
log.info("extension.register");
|
|
27
56
|
let runtimeMode: SquiggleConfig["mode"] | undefined;
|
|
57
|
+
let correctionScope = new AbortController();
|
|
58
|
+
const effectiveConfig = (cwd: string): SquiggleConfig => loadEffectiveConfig(cwd, runtimeMode);
|
|
28
59
|
|
|
29
60
|
pi.on("session_start", async (event, ctx) => {
|
|
61
|
+
correctionScope.abort();
|
|
62
|
+
correctionScope = new AbortController();
|
|
30
63
|
runtimeMode = restoreRuntimeMode(ctx);
|
|
31
64
|
log.info("session.start", { reason: event.reason, sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), runtimeMode });
|
|
32
65
|
});
|
|
33
66
|
|
|
67
|
+
pi.on("session_shutdown", async () => {
|
|
68
|
+
correctionScope.abort();
|
|
69
|
+
});
|
|
70
|
+
|
|
34
71
|
pi.registerCommand("squiggle", {
|
|
35
|
-
description: "Toggle squiggle on/off",
|
|
72
|
+
description: "Toggle squiggle on/off or configure its correction model",
|
|
73
|
+
getArgumentCompletions: (prefix) => ["toggle", "config", "--help"]
|
|
74
|
+
.filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
36
75
|
handler: async (args, ctx) => {
|
|
37
76
|
log.info("command.squiggle", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), args: args.trim() });
|
|
38
77
|
if (isHelpRequest(args)) return emitHelp(ctx, SQUIGGLE_HELP);
|
|
39
78
|
|
|
40
79
|
const command = args.trim().toLowerCase();
|
|
80
|
+
if (command === "config") {
|
|
81
|
+
const model = await chooseModel(ctx);
|
|
82
|
+
if (!model) return;
|
|
83
|
+
try {
|
|
84
|
+
saveModel(join(ctx.cwd, CONFIG_DIR_NAME, "squiggle.json"), model);
|
|
85
|
+
} catch {
|
|
86
|
+
ctx.ui.notify("Could not save Squiggle configuration. Check the config file and its permissions.", "error");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const effective = effectiveConfig(ctx.cwd);
|
|
90
|
+
ctx.ui.notify(`Saved ${model}. ${formatStatus(ctx, effective)}`,
|
|
91
|
+
effective.model === model ? "info" : "warning");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
41
94
|
if (command !== "toggle") {
|
|
42
|
-
ctx.ui.notify("Usage: /squiggle toggle", "warning");
|
|
95
|
+
ctx.ui.notify("Usage: /squiggle toggle | config", "warning");
|
|
43
96
|
return;
|
|
44
97
|
}
|
|
45
98
|
|
|
46
99
|
const config = loadEffectiveConfig(ctx.cwd, runtimeMode);
|
|
47
100
|
runtimeMode = config.mode === "on" ? "off" : "on";
|
|
101
|
+
if (runtimeMode === "off") {
|
|
102
|
+
correctionScope.abort();
|
|
103
|
+
correctionScope = new AbortController();
|
|
104
|
+
}
|
|
48
105
|
persistRuntimeMode(pi, runtimeMode);
|
|
49
|
-
ctx.ui.notify(formatStatus(ctx,
|
|
106
|
+
ctx.ui.notify(formatStatus(ctx, effectiveConfig(ctx.cwd)), "info");
|
|
50
107
|
},
|
|
51
108
|
});
|
|
52
109
|
|
|
@@ -55,19 +112,22 @@ export function registerSquiggle(
|
|
|
55
112
|
handler: async (args, ctx) => {
|
|
56
113
|
log.info("command.squiggle-status", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.() });
|
|
57
114
|
if (isHelpRequest(args)) return emitHelp(ctx, SQUIGGLE_STATUS_HELP);
|
|
58
|
-
ctx.ui.notify(formatStatus(ctx,
|
|
115
|
+
ctx.ui.notify(formatStatus(ctx, effectiveConfig(ctx.cwd)), "info");
|
|
59
116
|
},
|
|
60
117
|
});
|
|
61
118
|
|
|
62
119
|
pi.on("input", async (event, ctx) => {
|
|
63
120
|
if (event.source === "extension") return { action: "continue" };
|
|
64
121
|
|
|
65
|
-
const config =
|
|
122
|
+
const config = effectiveConfig(ctx.cwd);
|
|
66
123
|
if (config.mode === "off") return { action: "continue" };
|
|
67
124
|
if (!event.text.trim()) return { action: "continue" };
|
|
68
125
|
|
|
69
126
|
const stopIndicator = startSquiggleIndicator(ctx);
|
|
70
|
-
const
|
|
127
|
+
const cancellationSignal = ctx.signal
|
|
128
|
+
? AbortSignal.any([ctx.signal, correctionScope.signal])
|
|
129
|
+
: correctionScope.signal;
|
|
130
|
+
const corrected = await correctPrompt(event.text, ctx, config, log, cancellationSignal).finally(stopIndicator);
|
|
71
131
|
if (!corrected || corrected === event.text) return { action: "continue" };
|
|
72
132
|
|
|
73
133
|
if (ctx.hasUI) ctx.ui.notify(formatColoredDiff(event.text, corrected), "info");
|
|
@@ -102,22 +162,67 @@ Task:
|
|
|
102
162
|
const DEFAULT_CORRECTION_MODEL = "openai-codex/gpt-5.4-mini";
|
|
103
163
|
const DEFAULT_MAX_LLM_INPUT_CHARS = 500;
|
|
104
164
|
|
|
105
|
-
type
|
|
106
|
-
mode: "on" | "off";
|
|
107
|
-
model: string;
|
|
108
|
-
maxInputChars: number;
|
|
109
|
-
};
|
|
165
|
+
type CorrectionInterruption = "cancelled" | "timeout";
|
|
110
166
|
|
|
111
|
-
|
|
167
|
+
class CorrectionInterruptedError extends Error {
|
|
168
|
+
readonly kind: CorrectionInterruption;
|
|
169
|
+
|
|
170
|
+
constructor(kind: CorrectionInterruption, timeoutMs?: number) {
|
|
171
|
+
super(kind === "timeout" ? `Correction timed out after ${formatDuration(timeoutMs!)}.` : "Correction cancelled.");
|
|
172
|
+
this.kind = kind;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function correctWithModel(
|
|
177
|
+
input: string,
|
|
178
|
+
ctx: ExtensionContext,
|
|
179
|
+
config: SquiggleConfig,
|
|
180
|
+
log = createPithosLogger(),
|
|
181
|
+
signal?: AbortSignal,
|
|
182
|
+
completePrompt: typeof complete = complete,
|
|
183
|
+
): Promise<string | null> {
|
|
112
184
|
const model = selectCorrectionModel(ctx, config);
|
|
113
185
|
if (!model) return null;
|
|
114
186
|
if (input.length > config.maxInputChars) return null;
|
|
115
187
|
|
|
188
|
+
const started = Date.now();
|
|
189
|
+
const sessionId = (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.();
|
|
190
|
+
log.info("model.correct.start", { sessionId, inputChars: input.length, timeoutMs: config.timeoutMs, ...modelMetadata(model) });
|
|
191
|
+
|
|
192
|
+
if (signal?.aborted) {
|
|
193
|
+
log.warn("model.correct.cancelled", { sessionId, durationMs: Date.now() - started, inputChars: input.length, ...modelMetadata(model) });
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const requestController = new AbortController();
|
|
198
|
+
let interruptionKind: CorrectionInterruption | undefined;
|
|
199
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
200
|
+
let removeAbort: (() => void) | undefined;
|
|
201
|
+
const interruption = new Promise<never>((_resolve, reject) => {
|
|
202
|
+
timer = setTimeout(() => {
|
|
203
|
+
interruptionKind = "timeout";
|
|
204
|
+
requestController.abort();
|
|
205
|
+
reject(new CorrectionInterruptedError("timeout", config.timeoutMs));
|
|
206
|
+
}, config.timeoutMs);
|
|
207
|
+
const onAbort = () => {
|
|
208
|
+
if (interruptionKind) return;
|
|
209
|
+
interruptionKind = "cancelled";
|
|
210
|
+
requestController.abort();
|
|
211
|
+
reject(new CorrectionInterruptedError("cancelled"));
|
|
212
|
+
};
|
|
213
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
214
|
+
removeAbort = () => signal?.removeEventListener("abort", onAbort);
|
|
215
|
+
});
|
|
216
|
+
|
|
116
217
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
218
|
+
const auth = await Promise.race([
|
|
219
|
+
ctx.modelRegistry.getApiKeyAndHeaders(model),
|
|
220
|
+
interruption,
|
|
221
|
+
]);
|
|
222
|
+
if (!auth.ok || !auth.apiKey) {
|
|
223
|
+
log.warn("model.correct.unavailable", { sessionId, durationMs: Date.now() - started, inputChars: input.length, ...modelMetadata(model) });
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
121
226
|
|
|
122
227
|
const userMessage: UserMessage = {
|
|
123
228
|
role: "user",
|
|
@@ -125,14 +230,24 @@ async function correctWithModel(input: string, ctx: ExtensionContext, config: Sq
|
|
|
125
230
|
timestamp: Date.now(),
|
|
126
231
|
};
|
|
127
232
|
|
|
128
|
-
const response = await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
233
|
+
const response = await Promise.race([
|
|
234
|
+
completePrompt(
|
|
235
|
+
model,
|
|
236
|
+
{ systemPrompt: CORRECTION_PROMPT, messages: [userMessage] },
|
|
237
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal: requestController.signal },
|
|
238
|
+
),
|
|
239
|
+
interruption,
|
|
240
|
+
]);
|
|
133
241
|
|
|
134
242
|
if (response.stopReason === "aborted") {
|
|
135
|
-
|
|
243
|
+
const metadata = { sessionId, durationMs: Date.now() - started, inputChars: input.length, ...modelMetadata(model), usage: usageMetadata(response.usage) };
|
|
244
|
+
if (interruptionKind === "timeout") {
|
|
245
|
+
log.warn("model.correct.timeout", { ...metadata, timeoutMs: config.timeoutMs });
|
|
246
|
+
} else if (interruptionKind === "cancelled" || signal?.aborted) {
|
|
247
|
+
log.warn("model.correct.cancelled", metadata);
|
|
248
|
+
} else {
|
|
249
|
+
log.warn("model.correct.aborted", metadata);
|
|
250
|
+
}
|
|
136
251
|
return null;
|
|
137
252
|
}
|
|
138
253
|
|
|
@@ -141,20 +256,35 @@ async function correctWithModel(input: string, ctx: ExtensionContext, config: Sq
|
|
|
141
256
|
.map((c) => c.text)
|
|
142
257
|
.join("\n")
|
|
143
258
|
.trim();
|
|
144
|
-
log.info("model.correct.complete", { sessionId
|
|
259
|
+
log.info("model.correct.complete", { sessionId, durationMs: Date.now() - started, inputChars: input.length, changed: corrected !== input, ...modelMetadata(model), usage: usageMetadata(response.usage) });
|
|
145
260
|
return corrected;
|
|
146
261
|
} catch (error) {
|
|
147
|
-
|
|
262
|
+
if (interruptionKind === "timeout" || (error instanceof CorrectionInterruptedError && error.kind === "timeout")) {
|
|
263
|
+
log.warn("model.correct.timeout", { sessionId, durationMs: Date.now() - started, inputChars: input.length, timeoutMs: config.timeoutMs, ...modelMetadata(model) });
|
|
264
|
+
} else if (interruptionKind === "cancelled" || signal?.aborted || error instanceof CorrectionInterruptedError || (error instanceof Error && error.name === "AbortError")) {
|
|
265
|
+
log.warn("model.correct.cancelled", { sessionId, durationMs: Date.now() - started, inputChars: input.length, ...modelMetadata(model) });
|
|
266
|
+
} else {
|
|
267
|
+
log.warn("model.correct.error", { sessionId, durationMs: Date.now() - started, inputChars: input.length, ...modelMetadata(model), error: errorMetadata(error) });
|
|
268
|
+
}
|
|
148
269
|
return null;
|
|
270
|
+
} finally {
|
|
271
|
+
if (timer) clearTimeout(timer);
|
|
272
|
+
removeAbort?.();
|
|
149
273
|
}
|
|
150
274
|
}
|
|
151
275
|
|
|
276
|
+
function formatDuration(timeoutMs: number): string {
|
|
277
|
+
return timeoutMs % 1_000 === 0 ? `${timeoutMs / 1_000}s` : `${timeoutMs}ms`;
|
|
278
|
+
}
|
|
279
|
+
|
|
152
280
|
function loadConfig(cwd: string): SquiggleConfig {
|
|
153
281
|
const fileConfig = readConfigFile(cwd);
|
|
154
282
|
return {
|
|
155
283
|
mode: normalizeMode(process.env.SQUIGGLE_MODE ?? fileConfig.mode) ?? "on",
|
|
156
284
|
model: process.env.SQUIGGLE_MODEL ?? fileConfig.model ?? DEFAULT_CORRECTION_MODEL,
|
|
285
|
+
modelScope: process.env.SQUIGGLE_MODEL !== undefined ? "SQUIGGLE_MODEL" : fileConfig.model !== undefined ? "project" : "default",
|
|
157
286
|
maxInputChars: normalizePositiveInt(process.env.SQUIGGLE_MAX_CHARS ?? fileConfig.maxInputChars) ?? DEFAULT_MAX_LLM_INPUT_CHARS,
|
|
287
|
+
timeoutMs: normalizeTimeoutMs(process.env.SQUIGGLE_TIMEOUT_MS ?? fileConfig.timeoutMs) ?? DEFAULT_CORRECTION_TIMEOUT_MS,
|
|
158
288
|
};
|
|
159
289
|
}
|
|
160
290
|
|
|
@@ -172,16 +302,52 @@ function restoreRuntimeMode(ctx: ExtensionContext): SquiggleConfig["mode"] | und
|
|
|
172
302
|
return undefined;
|
|
173
303
|
}
|
|
174
304
|
|
|
305
|
+
function saveModel(path: string, model: string): void {
|
|
306
|
+
const current = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
307
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) throw new Error("Invalid config");
|
|
308
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
309
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
310
|
+
try {
|
|
311
|
+
writeFileSync(temporaryPath, `${JSON.stringify({ ...current, model }, null, 2)}\n`, { mode: 0o600 });
|
|
312
|
+
renameSync(temporaryPath, path);
|
|
313
|
+
} finally {
|
|
314
|
+
rmSync(temporaryPath, { force: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function chooseModel(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
319
|
+
if (!ctx.hasUI) {
|
|
320
|
+
emitHelp(ctx, "Squiggle configuration requires an interactive UI.");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const models = ctx.modelRegistry.getAvailable()
|
|
324
|
+
.filter((model) => ctx.modelRegistry.hasConfiguredAuth(model))
|
|
325
|
+
.sort((a, b) => `${a.provider}/${a.id}`.localeCompare(`${b.provider}/${b.id}`));
|
|
326
|
+
if (!models.length) {
|
|
327
|
+
ctx.ui.notify("No authenticated correction models are available. Configure a provider with /login first.", "error");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const choices = models.map((model) => `${model.provider}/${model.id} — ${model.name}`);
|
|
331
|
+
const selected = await ctx.ui.select("Exact correction model", choices);
|
|
332
|
+
const model = models[choices.indexOf(selected ?? "")];
|
|
333
|
+
return model ? `${model.provider}/${model.id}` : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
175
336
|
function persistRuntimeMode(pi: ExtensionAPI, mode: SquiggleConfig["mode"]): void {
|
|
176
337
|
pi.appendEntry("squiggle-mode", { mode });
|
|
177
338
|
}
|
|
178
339
|
|
|
179
340
|
function formatStatus(ctx: ExtensionContext, config: SquiggleConfig): string {
|
|
180
|
-
|
|
341
|
+
const unavailable = selectCorrectionModel(ctx, config) ? "" : "; unavailable — run /squiggle config";
|
|
342
|
+
const source = config.modelScope === "SQUIGGLE_MODEL" ? "; SQUIGGLE_MODEL" : "";
|
|
343
|
+
return `squiggle is ${config.mode} (${config.model}${source}${unavailable}).`;
|
|
181
344
|
}
|
|
182
345
|
|
|
183
346
|
function readConfigFile(cwd: string): Partial<SquiggleConfig> {
|
|
184
|
-
|
|
347
|
+
return readConfigPath(join(cwd, CONFIG_DIR_NAME, "squiggle.json"));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function readConfigPath(path: string): Partial<SquiggleConfig> {
|
|
185
351
|
if (!existsSync(path)) return {};
|
|
186
352
|
try {
|
|
187
353
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
@@ -189,6 +355,7 @@ function readConfigFile(cwd: string): Partial<SquiggleConfig> {
|
|
|
189
355
|
mode: typeof parsed.mode === "string" ? normalizeMode(parsed.mode) : undefined,
|
|
190
356
|
model: typeof parsed.model === "string" ? parsed.model : undefined,
|
|
191
357
|
maxInputChars: normalizePositiveInt(parsed.maxInputChars),
|
|
358
|
+
timeoutMs: normalizeTimeoutMs(parsed.timeoutMs),
|
|
192
359
|
};
|
|
193
360
|
} catch {
|
|
194
361
|
return {};
|
|
@@ -204,13 +371,20 @@ function normalizePositiveInt(value: unknown): number | undefined {
|
|
|
204
371
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
205
372
|
}
|
|
206
373
|
|
|
374
|
+
export function normalizeTimeoutMs(value: unknown): number | undefined {
|
|
375
|
+
const parsed = normalizePositiveInt(value);
|
|
376
|
+
return parsed !== undefined && parsed >= MIN_CORRECTION_TIMEOUT_MS && parsed <= MAX_CORRECTION_TIMEOUT_MS
|
|
377
|
+
? parsed
|
|
378
|
+
: undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
207
381
|
function selectCorrectionModel(ctx: ExtensionContext, config: SquiggleConfig) {
|
|
208
382
|
const configured = parseModelSpec(config.model);
|
|
209
383
|
if (configured) {
|
|
210
384
|
const model = ctx.modelRegistry.find(configured.provider, configured.model);
|
|
211
385
|
if (model) return model;
|
|
212
386
|
}
|
|
213
|
-
return
|
|
387
|
+
return undefined;
|
|
214
388
|
}
|
|
215
389
|
|
|
216
390
|
function parseModelSpec(spec: string): { provider: string; model: string } | null {
|
|
@@ -219,10 +393,6 @@ function parseModelSpec(spec: string): { provider: string; model: string } | nul
|
|
|
219
393
|
return { provider: spec.slice(0, slash), model: spec.slice(slash + 1) };
|
|
220
394
|
}
|
|
221
395
|
|
|
222
|
-
function formatModel(model: ReturnType<typeof selectCorrectionModel>): string {
|
|
223
|
-
return model ? `${model.provider}/${model.id}` : "no model";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
396
|
function startSquiggleIndicator(ctx: ExtensionContext): () => void {
|
|
227
397
|
if (!ctx.hasUI) return () => {};
|
|
228
398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithos-kit/squiggle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Quietly polish grammar and spelling in your Pi prompts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"summary": "Quietly polish grammar and spelling in user prompts.",
|
|
32
32
|
"minimumPi": ">=0.83.0",
|
|
33
33
|
"commands": [
|
|
34
|
-
{ "name": "squiggle", "usage": "/squiggle [toggle|--help]", "summary": "Toggle prompt correction." },
|
|
34
|
+
{ "name": "squiggle", "usage": "/squiggle [toggle|config|--help]", "summary": "Toggle prompt correction or configure its model." },
|
|
35
35
|
{ "name": "squiggle-status", "usage": "/squiggle-status [--help]", "summary": "Show correction status and configuration." }
|
|
36
36
|
],
|
|
37
37
|
"tools": [],
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"themes": [],
|
|
41
41
|
"agents": [],
|
|
42
42
|
"configuration": [
|
|
43
|
-
{ "kind": "file", "key": ".pi/squiggle.json", "summary": "Project correction mode, model,
|
|
43
|
+
{ "kind": "file", "key": ".pi/squiggle.json", "summary": "Project correction mode, model, input limit, and timeout." },
|
|
44
44
|
{ "kind": "environment", "key": "SQUIGGLE_MODE", "summary": "Override correction mode." },
|
|
45
45
|
{ "kind": "environment", "key": "SQUIGGLE_MODEL", "summary": "Override the correction model." },
|
|
46
|
-
{ "kind": "environment", "key": "SQUIGGLE_MAX_CHARS", "summary": "Override the maximum corrected input length." }
|
|
46
|
+
{ "kind": "environment", "key": "SQUIGGLE_MAX_CHARS", "summary": "Override the maximum corrected input length." },
|
|
47
|
+
{ "kind": "environment", "key": "SQUIGGLE_TIMEOUT_MS", "summary": "Override the bounded correction timeout." }
|
|
47
48
|
]
|
|
48
49
|
},
|
|
49
50
|
"pi": {
|