feat(KB-184): add model presets for task creation

- Add model preset types and settings fields with API validation
- Create model preset utility helpers for size-based auto-selection
- Add settings UI for managing model presets (create, edit, delete)
- Integrate preset selection into NewTaskModal with custom override option
- Add inline preset selection to InlineCreateCard component
- Persist modelPresetId in task store and preserve through archive/unarchive
- Add changeset and update AGENTS.md with feature documentation
This commit is contained in:
gsxdsm
2026-03-31 02:04:45 -07:00
parent 82d8ebb82b
commit 714f4bc415
17 changed files with 959 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES } from "./types.js";
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeResult, Settings, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, SteeringComment, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult } from "./types.js";
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeResult, Settings, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, SteeringComment, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset } from "./types.js";
export { TaskStore } from "./store.js";
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
export {

View File

@@ -121,6 +121,18 @@ describe("TaskStore", () => {
expect(detail.breakIntoSubtasks).toBe(true);
});
it("persists modelPresetId when provided during task creation", async () => {
const task = await store.createTask({
description: "Preset task",
modelPresetId: "budget",
});
expect(task.modelPresetId).toBe("budget");
const detail = await store.getTask(task.id);
expect(detail.modelPresetId).toBe("budget");
});
it("leaves breakIntoSubtasks unset by default", async () => {
const task = await store.createTask({
description: "Regular task",

View File

@@ -301,6 +301,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
column: input.column || "triage",
dependencies: input.dependencies || [],
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
modelPresetId: input.modelPresetId,
modelProvider: input.modelProvider,
modelId: input.modelId,
validatorModelProvider: input.validatorModelProvider,
@@ -349,6 +350,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
title: sourceTask.title,
description: `${sourceTask.description}\n\n(Duplicated from ${id})`,
column: "triage",
modelPresetId: sourceTask.modelPresetId,
dependencies: [], // Fresh task should have no dependencies
steps: [], // Reset execution state
currentStep: 0,
@@ -1053,6 +1055,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
updatedAt: task.updatedAt,
columnMovedAt: task.columnMovedAt,
archivedAt: task.columnMovedAt,
modelPresetId: task.modelPresetId,
modelProvider: task.modelProvider,
modelId: task.modelId,
validatorModelProvider: task.validatorModelProvider,
@@ -1794,6 +1797,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
createdAt: entry.createdAt,
updatedAt: new Date().toISOString(),
columnMovedAt: entry.columnMovedAt,
modelPresetId: entry.modelPresetId,
modelProvider: entry.modelProvider,
modelId: entry.modelId,
validatorModelProvider: entry.validatorModelProvider,

View File

@@ -29,6 +29,15 @@ export type ColorTheme = (typeof COLOR_THEMES)[number];
export type PrStatus = "open" | "closed" | "merged";
export type MergeStrategy = "direct" | "pull-request";
export interface ModelPreset {
id: string;
name: string;
executorProvider?: string;
executorModelId?: string;
validatorProvider?: string;
validatorModelId?: string;
}
export interface PrInfo {
url: string;
number: number;
@@ -166,6 +175,8 @@ export interface Task {
log: TaskLogEntry[];
size?: "S" | "M" | "L";
reviewLevel?: number;
/** Model preset selected during task creation. Presets resolve to concrete model overrides at creation time. */
modelPresetId?: string;
/** AI model provider override for the executor agent (e.g., "anthropic").
* Must be set together with `modelId`. When both model fields are undefined,
* the executor uses global settings defaults. */
@@ -205,6 +216,8 @@ export interface TaskCreateInput {
column?: Column;
dependencies?: string[];
breakIntoSubtasks?: boolean;
/** Model preset selected during task creation. Presets resolve to concrete model overrides at creation time. */
modelPresetId?: string;
/** AI model provider override for the executor agent (e.g., "anthropic").
* Must be set together with `modelId`. When both model fields are undefined,
* the executor uses global settings defaults. */
@@ -306,6 +319,12 @@ export interface Settings {
* Must be set together with `validatorProvider`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */
validatorModelId?: string;
/** Reusable model configuration presets for task creation. */
modelPresets?: ModelPreset[];
/** When true, task creation UIs automatically recommend/apply a preset based on task size. */
autoSelectModelPreset?: boolean;
/** Mapping of task sizes to preset IDs used for auto-selection during task creation. */
defaultPresetBySize?: { S?: string; M?: string; L?: string };
/** Default thinking effort level for AI agent sessions.
* Controls how much reasoning effort the model uses — higher levels
* produce better results but cost more. When undefined, the engine
@@ -362,6 +381,9 @@ export const DEFAULT_SETTINGS: Settings = {
planningModelId: undefined,
validatorProvider: undefined,
validatorModelId: undefined,
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
defaultThinkingLevel: undefined,
autoResolveConflicts: true,
smartConflictResolution: true,
@@ -447,7 +469,8 @@ export interface ArchivedTaskEntry {
columnMovedAt?: string;
/** Timestamp when the task was archived to the log */
archivedAt: string;
/** Optional: model override fields for executor and validator */
/** Optional: model preset and override fields for executor and validator */
modelPresetId?: string;
modelProvider?: string;
modelId?: string;
validatorModelProvider?: string;