feat: add answer extraction replay commands
This commit is contained in:
+69
-4
@@ -1,7 +1,20 @@
|
||||
import { DEFAULT_ASK_KEYMAPS } from "../constants/keymaps.ts";
|
||||
import type { AskConfig, AskConfigFileV1 } from "./schema.ts";
|
||||
import type { AskConfig, AskConfigFileV2 } from "./schema.ts";
|
||||
|
||||
const DEFAULT_EXTRACTION_RETRIES = 1;
|
||||
const MAX_EXTRACTION_RETRIES = 3;
|
||||
const DEFAULT_EXTRACTION_TIMEOUT_MS = 30_000;
|
||||
|
||||
export const DEFAULT_ASK_CONFIG: AskConfig = {
|
||||
answer: {
|
||||
extractionModels: [
|
||||
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
||||
{ provider: "github-copilot", id: "gpt-5.4-mini" },
|
||||
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
||||
],
|
||||
extractionRetries: DEFAULT_EXTRACTION_RETRIES,
|
||||
extractionTimeoutMs: DEFAULT_EXTRACTION_TIMEOUT_MS,
|
||||
},
|
||||
behaviour: {
|
||||
autoSubmitWhenAnsweredWithoutNotes: false,
|
||||
confirmDismissWhenDirty: true,
|
||||
@@ -14,9 +27,24 @@ export const DEFAULT_ASK_CONFIG: AskConfig = {
|
||||
};
|
||||
|
||||
export function normalizeAskConfig(
|
||||
config?: Partial<AskConfigFileV1> | AskConfig
|
||||
config?: Partial<AskConfigFileV2> | AskConfig
|
||||
): AskConfig {
|
||||
return {
|
||||
answer: {
|
||||
extractionModels:
|
||||
config?.answer?.extractionModels?.filter(isValidModelPreference) ??
|
||||
DEFAULT_ASK_CONFIG.answer.extractionModels,
|
||||
extractionRetries: clampInteger(
|
||||
config?.answer?.extractionRetries,
|
||||
0,
|
||||
MAX_EXTRACTION_RETRIES,
|
||||
DEFAULT_EXTRACTION_RETRIES
|
||||
),
|
||||
extractionTimeoutMs: positiveNumberOrDefault(
|
||||
config?.answer?.extractionTimeoutMs,
|
||||
DEFAULT_EXTRACTION_TIMEOUT_MS
|
||||
),
|
||||
},
|
||||
behaviour: {
|
||||
autoSubmitWhenAnsweredWithoutNotes:
|
||||
config?.behaviour?.autoSubmitWhenAnsweredWithoutNotes ??
|
||||
@@ -38,10 +66,15 @@ export function normalizeAskConfig(
|
||||
};
|
||||
}
|
||||
|
||||
export function toAskConfigFileV1(config: AskConfig): AskConfigFileV1 {
|
||||
export function toAskConfigFileV2(config: AskConfig): AskConfigFileV2 {
|
||||
const normalized = normalizeAskConfig(config);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
answer: {
|
||||
extractionModels: normalized.answer.extractionModels,
|
||||
extractionRetries: normalized.answer.extractionRetries,
|
||||
extractionTimeoutMs: normalized.answer.extractionTimeoutMs,
|
||||
},
|
||||
behaviour: {
|
||||
autoSubmitWhenAnsweredWithoutNotes:
|
||||
normalized.behaviour.autoSubmitWhenAnsweredWithoutNotes,
|
||||
@@ -60,3 +93,35 @@ export function toAskConfigFileV1(config: AskConfig): AskConfigFileV1 {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isValidModelPreference(value: unknown): value is {
|
||||
id: string;
|
||||
provider: string;
|
||||
} {
|
||||
return (
|
||||
!!value &&
|
||||
typeof value === "object" &&
|
||||
typeof (value as { id?: unknown }).id === "string" &&
|
||||
(value as { id: string }).id.trim().length > 0 &&
|
||||
typeof (value as { provider?: unknown }).provider === "string" &&
|
||||
(value as { provider: string }).provider.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function clampInteger(
|
||||
value: unknown,
|
||||
min: number,
|
||||
max: number,
|
||||
fallback: number
|
||||
): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(min, Math.min(max, Math.trunc(value)));
|
||||
}
|
||||
|
||||
function positiveNumberOrDefault(value: unknown, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
type AskConfigVersionMigrationResult,
|
||||
migrateAskConfigFileToCurrent,
|
||||
} from "./migrations/index.ts";
|
||||
import type { AskConfig, AskConfigFileV1 } from "./schema.ts";
|
||||
import { validateAskConfigFileV1 } from "./schema.ts";
|
||||
import type { AskConfig, AskConfigFileV2 } from "./schema.ts";
|
||||
import { validateAskConfigFileV2 } from "./schema.ts";
|
||||
|
||||
export class AskConfigMigrationError extends Error {
|
||||
readonly reason: "invalid_or_unsupported" | "migration_failed";
|
||||
@@ -42,14 +42,14 @@ export function migrateAskConfig(raw: unknown): AskConfigMigrationResult {
|
||||
);
|
||||
}
|
||||
|
||||
if (!validateAskConfigFileV1.Check(migratedFile.config)) {
|
||||
if (!validateAskConfigFileV2.Check(migratedFile.config)) {
|
||||
throw new AskConfigMigrationError(
|
||||
"Config was invalid or unsupported.",
|
||||
"invalid_or_unsupported"
|
||||
);
|
||||
}
|
||||
|
||||
const currentFile = migratedFile.config as AskConfigFileV1;
|
||||
const currentFile = migratedFile.config as AskConfigFileV2;
|
||||
const config = normalizeAskConfig(currentFile);
|
||||
const keymapsResult = normalizeConfiguredKeymaps(
|
||||
currentFile.keymaps as
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import type { AskConfigMigration, VersionedAskConfigFile } from "./types.ts";
|
||||
|
||||
export const CURRENT_ASK_CONFIG_SCHEMA_VERSION = 1;
|
||||
export const CURRENT_ASK_CONFIG_SCHEMA_VERSION = 2;
|
||||
|
||||
const ASK_CONFIG_MIGRATIONS: AskConfigMigration[] = [];
|
||||
const ASK_CONFIG_MIGRATIONS: AskConfigMigration[] = [
|
||||
{
|
||||
from: 1,
|
||||
to: 2,
|
||||
migrate: (config) => ({
|
||||
...config,
|
||||
schemaVersion: 2,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export class AskConfigVersionMigrationError extends Error {
|
||||
readonly reason: "invalid_or_unsupported" | "migration_failed";
|
||||
|
||||
+36
-6
@@ -1,8 +1,22 @@
|
||||
import { type Static, Type } from "typebox";
|
||||
import { Compile } from "typebox/compile";
|
||||
|
||||
export const AskConfigFileV1Schema = Type.Object({
|
||||
schemaVersion: Type.Literal(1),
|
||||
const AskAnswerModelPreferenceSchema = Type.Object({
|
||||
id: Type.String(),
|
||||
provider: Type.String(),
|
||||
});
|
||||
|
||||
export const AskConfigFileV2Schema = Type.Object({
|
||||
schemaVersion: Type.Literal(2),
|
||||
answer: Type.Optional(
|
||||
Type.Object({
|
||||
extractionModels: Type.Optional(
|
||||
Type.Array(AskAnswerModelPreferenceSchema)
|
||||
),
|
||||
extractionRetries: Type.Optional(Type.Number()),
|
||||
extractionTimeoutMs: Type.Optional(Type.Number()),
|
||||
})
|
||||
),
|
||||
behaviour: Type.Optional(
|
||||
Type.Object({
|
||||
autoSubmitWhenAnsweredWithoutNotes: Type.Optional(Type.Boolean()),
|
||||
@@ -14,16 +28,27 @@ export const AskConfigFileV1Schema = Type.Object({
|
||||
keymaps: Type.Optional(
|
||||
Type.Object({
|
||||
cancel: Type.Optional(Type.String()),
|
||||
dismiss: Type.Optional(Type.String()),
|
||||
toggle: Type.Optional(Type.String()),
|
||||
confirm: Type.Optional(Type.String()),
|
||||
dismiss: Type.Optional(Type.String()),
|
||||
optionNote: Type.Optional(Type.String()),
|
||||
questionNote: Type.Optional(Type.String()),
|
||||
toggle: Type.Optional(Type.String()),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type AskConfigFileV1 = Static<typeof AskConfigFileV1Schema>;
|
||||
export type AskConfigFileV2 = Static<typeof AskConfigFileV2Schema>;
|
||||
export type AskConfigFileV1 = Omit<
|
||||
AskConfigFileV2,
|
||||
"answer" | "schemaVersion"
|
||||
> & {
|
||||
schemaVersion: 1;
|
||||
};
|
||||
|
||||
export interface AskAnswerModelPreference {
|
||||
id: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface AskConfigKeymaps {
|
||||
cancel: string;
|
||||
@@ -35,6 +60,11 @@ export interface AskConfigKeymaps {
|
||||
}
|
||||
|
||||
export interface AskConfig {
|
||||
answer: {
|
||||
extractionModels: AskAnswerModelPreference[];
|
||||
extractionRetries: number;
|
||||
extractionTimeoutMs: number;
|
||||
};
|
||||
behaviour: {
|
||||
autoSubmitWhenAnsweredWithoutNotes: boolean;
|
||||
confirmDismissWhenDirty: boolean;
|
||||
@@ -44,4 +74,4 @@ export interface AskConfig {
|
||||
keymaps: AskConfigKeymaps;
|
||||
}
|
||||
|
||||
export const validateAskConfigFileV1 = Compile(AskConfigFileV1Schema);
|
||||
export const validateAskConfigFileV2 = Compile(AskConfigFileV2Schema);
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
DEFAULT_ASK_CONFIG,
|
||||
normalizeAskConfig,
|
||||
toAskConfigFileV1,
|
||||
toAskConfigFileV2,
|
||||
} from "./defaults.ts";
|
||||
import { AskConfigMigrationError, migrateAskConfig } from "./migrate.ts";
|
||||
import { migrateAskConfigPathIfNeeded } from "./path-migrations.ts";
|
||||
@@ -63,10 +63,10 @@ export class AskConfigStore {
|
||||
return (await this.ensureLoaded()).config;
|
||||
}
|
||||
|
||||
async save(config: AskConfig): Promise<AskConfig> {
|
||||
async save(config: AskConfig | Partial<AskConfig>): Promise<AskConfig> {
|
||||
const normalized = normalizeAskConfig(config);
|
||||
const content = JSON.stringify(
|
||||
toAskConfigFileV1(normalized),
|
||||
toAskConfigFileV2(normalized),
|
||||
null,
|
||||
2
|
||||
).concat("\n");
|
||||
|
||||
Reference in New Issue
Block a user