diff --git a/packages/handoff/src/handoff-command.ts b/packages/handoff/src/handoff-command.ts index fbdad35d6559b9cb4bc423bc2f1b98098c61fbc3..3d5aae7f1caf30f754651c3c41169ad2d4009ba9 100644 --- a/packages/handoff/src/handoff-command.ts +++ b/packages/handoff/src/handoff-command.ts @@ -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(); diff --git a/packages/handoff/src/index.ts b/packages/handoff/src/index.ts index a619ff6a09862d818a578a7baafda421f06d63a8..b7ecfb6198f21ea5e4b41691d3ffb5b79ab0c194 100644 --- a/packages/handoff/src/index.ts +++ b/packages/handoff/src/index.ts @@ -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); }); diff --git a/packages/handoff/src/model-utils.ts b/packages/handoff/src/model-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..34bbefb1ed82649edfdc539e8418057786ded4c0 --- /dev/null +++ b/packages/handoff/src/model-utils.ts @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 { + 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 { + const result = await parseAndSetModel(spec, ctx, pi); + if (!result.ok) { + ctx.ui.notify(result.detail, "warning"); + } + return result; +}