Detailed changes
@@ -12,6 +12,7 @@ import type {
import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
import { extractCandidateFiles, extractLoadedSkills, resolveExtractionModel } from "./session-analysis.js";
import { type HandoffExtraction, assembleHandoffDraft, extractHandoffContext } from "./handoff-extraction.js";
+import { parseAndSetModelWithNotify } from "./model-utils.js";
export function registerHandoffCommand(pi: ExtensionAPI, startCountdown: (ctx: ExtensionContext) => void) {
pi.registerCommand("handoff", {
@@ -120,19 +121,7 @@ export function registerHandoffCommand(pi: ExtensionAPI, startCountdown: (ctx: E
// Apply -model if specified
if (newSessionModel) {
- const slashIdx = newSessionModel.indexOf("/");
- if (slashIdx > 0) {
- const provider = newSessionModel.slice(0, slashIdx);
- const modelId = newSessionModel.slice(slashIdx + 1);
- const model = ctx.modelRegistry.find(provider, modelId);
- if (model) {
- await pi.setModel(model);
- } else {
- ctx.ui.notify(`Unknown model: ${newSessionModel}`, "warning");
- }
- } else {
- ctx.ui.notify(`Invalid model format "${newSessionModel}", expected provider/modelId`, "warning");
- }
+ await parseAndSetModelWithNotify(newSessionModel, ctx, pi);
}
const newSessionFile = ctx.sessionManager.getSessionFile();
@@ -5,9 +5,10 @@
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import type { SessionManager } from "@mariozechner/pi-coding-agent";
-import { Key, matchesKey } from "@mariozechner/pi-tui";
+import { Key, matchesKey, parseKey } from "@mariozechner/pi-tui";
import { registerHandoffCommand } from "./handoff-command.js";
import { registerHandoffTool, type PendingHandoff } from "./handoff-tool.js";
+import { parseAndSetModelWithNotify } from "./model-utils.js";
import { registerSessionQueryTool } from "./session-query-tool.js";
const STATUS_KEY = "handoff";
@@ -20,22 +21,6 @@ type PendingAutoSubmit = {
unsubscribeInput: () => void;
};
-function isEditableInput(data: string): boolean {
- if (!data) return false;
- if (data.length === 1) {
- const code = data.charCodeAt(0);
- if (code >= 32 && code !== 127) return true;
- if (code === 8 || code === 13) return true;
- }
-
- if (data === "\n" || data === "\r") return true;
- if (data === "\x7f") return true;
-
- if (data.length > 1 && !data.startsWith("\x1b")) return true;
-
- return false;
-}
-
function statusLine(ctx: ExtensionContext, seconds: number): string {
const accent = ctx.ui.theme.fg("accent", `handoff auto-submit in ${seconds}s`);
const hint = ctx.ui.theme.fg("dim", "(type to edit, Esc to cancel)");
@@ -107,7 +92,7 @@ export default function (pi: ExtensionAPI) {
return { consume: true };
}
- if (isEditableInput(data)) {
+ if (parseKey(data) !== undefined) {
clearPending(ctx, "Handoff auto-submit stopped (editing)");
}
@@ -195,13 +180,7 @@ export default function (pi: ExtensionAPI) {
startCountdown(ctx);
if (newModel) {
- const slashIdx = newModel.indexOf("/");
- if (slashIdx > 0) {
- const provider = newModel.slice(0, slashIdx);
- const modelId = newModel.slice(slashIdx + 1);
- const model = ctx.modelRegistry.find(provider, modelId);
- if (model) await pi.setModel(model);
- }
+ await parseAndSetModelWithNotify(newModel, ctx, pi);
}
}, 0);
});
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: MIT
+
+import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
+
+export type SetModelResult =
+ | { ok: true }
+ | { ok: false; reason: "invalid-format" | "unknown-model" | "set-failed"; detail: string };
+
+/**
+ * Parse a "provider/modelId" string, look it up in the model registry, and
+ * set it as the active model. Returns a result describing what happened.
+ */
+export async function parseAndSetModel(spec: string, ctx: ExtensionContext, pi: ExtensionAPI): Promise<SetModelResult> {
+ const slashIdx = spec.indexOf("/");
+ if (slashIdx <= 0 || slashIdx === spec.length - 1) {
+ return { ok: false, reason: "invalid-format", detail: `Invalid model format "${spec}", expected provider/modelId` };
+ }
+
+ const provider = spec.slice(0, slashIdx);
+ const modelId = spec.slice(slashIdx + 1);
+ const model = ctx.modelRegistry.find(provider, modelId);
+
+ if (!model) {
+ return { ok: false, reason: "unknown-model", detail: `Unknown model: ${spec}` };
+ }
+
+ try {
+ await pi.setModel(model);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return { ok: false, reason: "set-failed", detail: `Failed to set model ${spec}: ${msg}` };
+ }
+
+ return { ok: true };
+}
+
+/**
+ * Convenience wrapper: parse and set the model, notifying the user on failure.
+ */
+export async function parseAndSetModelWithNotify(
+ spec: string,
+ ctx: ExtensionContext,
+ pi: ExtensionAPI,
+): Promise<SetModelResult> {
+ const result = await parseAndSetModel(spec, ctx, pi);
+ if (!result.ok) {
+ ctx.ui.notify(result.detail, "warning");
+ }
+ return result;
+}