Files
oh-my-pi/packages/coding-agent/src/config/settings-schema.ts
T
can1357 5277e44139 feat(coding-agent): added canonical aliases for model role resolution
- Added canonical model equivalence types, cache helpers, and registry APIs for provider variant lookup.
- Changed model resolution to apply canonical ID overrides/excludes with provider order before fallback matching.
- Added canonical and provider model views in list-models and selector UI with canonical sorting/persistence.
- Updated role/model persistence to store selectors while runtime now resolves concrete canonical-backed provider models.
2026-04-11 08:28:50 +02:00

1932 lines
50 KiB
TypeScript

import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
/** Unified settings schema - single source of truth for all settings.
* Unified settings schema - single source of truth for all settings.
*
* Each setting is defined once here with:
* - Type and default value
* - Optional UI metadata (label, description, tab)
*
* The Settings singleton provides type-safe path-based access:
* settings.get("compaction.enabled") // => boolean
* settings.set("theme.dark", "titanium") // sync, saves in background
*/
// ═══════════════════════════════════════════════════════════════════════════
// Schema Definition Types
// ═══════════════════════════════════════════════════════════════════════════
export type SettingTab =
| "appearance"
| "model"
| "interaction"
| "context"
| "editing"
| "tools"
| "tasks"
| "providers";
/** Tab display metadata - icon is resolved via theme.symbol() */
export type TabMetadata = { label: string; icon: `tab.${string}` };
/** Ordered list of tabs for UI rendering */
export const SETTING_TABS: SettingTab[] = [
"appearance",
"model",
"interaction",
"context",
"editing",
"tools",
"tasks",
"providers",
];
/** Tab display metadata - icon is a symbol key from theme.ts (tab.*) */
export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${string}` }> = {
appearance: { label: "Appearance", icon: "tab.appearance" },
model: { label: "Model", icon: "tab.model" },
interaction: { label: "Interaction", icon: "tab.interaction" },
context: { label: "Context", icon: "tab.context" },
editing: { label: "Editing", icon: "tab.editing" },
tools: { label: "Tools", icon: "tab.tools" },
tasks: { label: "Tasks", icon: "tab.tasks" },
providers: { label: "Providers", icon: "tab.providers" },
};
/** Status line segment identifiers */
export type StatusLineSegmentId =
| "pi"
| "model"
| "plan_mode"
| "path"
| "git"
| "pr"
| "subagents"
| "token_in"
| "token_out"
| "token_total"
| "token_rate"
| "cost"
| "context_pct"
| "context_total"
| "time_spent"
| "time"
| "session"
| "hostname"
| "cache_read"
| "cache_write";
interface UiMetadata {
tab: SettingTab;
label: string;
description: string;
/** For enum/submenu - display as inline toggle vs dropdown */
submenu?: boolean;
/** Condition function name - setting only shown when true */
condition?: string;
}
interface BooleanDef {
type: "boolean";
default: boolean;
ui?: UiMetadata;
}
interface StringDef {
type: "string";
default: string | undefined;
ui?: UiMetadata;
}
interface NumberDef {
type: "number";
default: number;
ui?: UiMetadata;
}
interface EnumDef<T extends readonly string[]> {
type: "enum";
values: T;
default: T[number];
ui?: UiMetadata;
}
interface ArrayDef<T> {
type: "array";
default: T[];
ui?: UiMetadata;
}
interface RecordDef<T> {
type: "record";
default: Record<string, T>;
ui?: UiMetadata;
}
type SettingDef =
| BooleanDef
| StringDef
| NumberDef
| EnumDef<readonly string[]>
| ArrayDef<unknown>
| RecordDef<unknown>;
// ═══════════════════════════════════════════════════════════════════════════
// Schema Definition
// ═══════════════════════════════════════════════════════════════════════════
export interface ModelTagDef {
name: string;
color?: string;
}
export interface ModelTagsSettings {
[key: string]: ModelTagDef;
}
// Typed defaults for array/record settings — named constants avoid `as` casts
// under `as const` while still letting SettingValue infer the correct element type.
const EMPTY_STRING_ARRAY: string[] = [];
const EMPTY_STRING_RECORD: Record<string, string> = {};
const DEFAULT_CYCLE_ORDER: string[] = ["smol", "default", "slow"];
const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {};
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
{
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
tool: "read",
message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
},
{
pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
tool: "grep",
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
},
{
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
tool: "find",
message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
},
{
pattern: "^\\s*sed\\s+(-i|--in-place)",
tool: "edit",
message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
},
{
pattern: "^\\s*perl\\s+.*-[pn]?i",
tool: "edit",
message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
},
{
pattern: "^\\s*awk\\s+.*-i\\s+inplace",
tool: "edit",
message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
},
{
pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
tool: "write",
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
},
];
export const SETTINGS_SCHEMA = {
// ────────────────────────────────────────────────────────────────────────
// General settings (no UI)
// ────────────────────────────────────────────────────────────────────────
lastChangelogVersion: { type: "string", default: undefined },
autoResume: {
type: "boolean",
default: false,
ui: {
tab: "interaction",
label: "Auto Resume",
description: "Automatically resume the most recent session in the current directory",
},
},
shellPath: { type: "string", default: undefined },
extensions: { type: "array", default: EMPTY_STRING_ARRAY },
"marketplace.autoUpdate": {
type: "enum",
values: ["off", "notify", "auto"] as const,
default: "notify",
ui: {
tab: "tools",
label: "Marketplace Auto-Update",
description: "Check for plugin updates on startup (off/notify/auto)",
submenu: true,
},
},
enabledModels: { type: "array", default: EMPTY_STRING_ARRAY },
disabledProviders: { type: "array", default: EMPTY_STRING_ARRAY },
disabledExtensions: { type: "array", default: EMPTY_STRING_ARRAY },
modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
modelProviderOrder: { type: "array", default: EMPTY_STRING_ARRAY },
cycleOrder: { type: "array", default: DEFAULT_CYCLE_ORDER },
// ────────────────────────────────────────────────────────────────────────
// Appearance
// ────────────────────────────────────────────────────────────────────────
// Theme
"theme.dark": {
type: "string",
default: "titanium",
ui: {
tab: "appearance",
label: "Dark Theme",
description: "Theme used when terminal has dark background",
submenu: true,
},
},
"theme.light": {
type: "string",
default: "light",
ui: {
tab: "appearance",
label: "Light Theme",
description: "Theme used when terminal has light background",
submenu: true,
},
},
symbolPreset: {
type: "enum",
values: ["unicode", "nerd", "ascii"] as const,
default: "unicode",
ui: { tab: "appearance", label: "Symbol Preset", description: "Icon/symbol style", submenu: true },
},
colorBlindMode: {
type: "boolean",
default: false,
ui: {
tab: "appearance",
label: "Color-Blind Mode",
description: "Use blue instead of green for diff additions",
},
},
// Status line
"statusLine.preset": {
type: "enum",
values: ["default", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
default: "default",
ui: {
tab: "appearance",
label: "Status Line Preset",
description: "Pre-built status line configurations",
submenu: true,
},
},
"statusLine.separator": {
type: "enum",
values: ["powerline", "powerline-thin", "slash", "pipe", "block", "none", "ascii"] as const,
default: "powerline-thin",
ui: {
tab: "appearance",
label: "Status Line Separator",
description: "Style of separators between segments",
submenu: true,
},
},
"tools.artifactSpillThreshold": {
type: "number",
default: 50,
ui: {
tab: "tools",
label: "Artifact spill threshold (KB)",
description: "Tool output above this size is saved as an artifact; tail is kept inline",
submenu: true,
},
},
"tools.artifactTailBytes": {
type: "number",
default: 20,
ui: {
tab: "tools",
label: "Artifact tail size (KB)",
description: "Amount of tail content kept inline when output spills to artifact",
submenu: true,
},
},
"tools.artifactTailLines": {
type: "number",
default: 500,
ui: {
tab: "tools",
label: "Artifact tail lines",
description: "Maximum lines of tail content kept inline when output spills to artifact",
submenu: true,
},
},
"statusLine.showHookStatus": {
type: "boolean",
default: true,
ui: {
tab: "appearance",
label: "Show Hook Status",
description: "Display hook status messages below status line",
},
},
"statusLine.leftSegments": { type: "array", default: [] as StatusLineSegmentId[] },
"statusLine.rightSegments": { type: "array", default: [] as StatusLineSegmentId[] },
"statusLine.segmentOptions": { type: "record", default: {} as Record<string, unknown> },
// Images and terminal
"terminal.showImages": {
type: "boolean",
default: true,
ui: {
tab: "appearance",
label: "Show Inline Images",
description: "Render images inline in terminal",
condition: "hasImageProtocol",
},
},
"images.autoResize": {
type: "boolean",
default: true,
ui: {
tab: "appearance",
label: "Auto-Resize Images",
description: "Resize large images to 2000x2000 max for better model compatibility",
},
},
"images.blockImages": {
type: "boolean",
default: false,
ui: { tab: "appearance", label: "Block Images", description: "Prevent images from being sent to LLM providers" },
},
"tui.maxInlineImageColumns": {
type: "number",
default: 100,
description:
"Maximum width in terminal columns for inline images (default 100). Set to 0 for unlimited (bounded only by terminal width).",
},
"tui.maxInlineImageRows": {
type: "number",
default: 20,
description:
"Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).",
},
// Display rendering
"display.tabWidth": {
type: "number",
default: 3,
ui: {
tab: "appearance",
label: "Tab Width",
description: "Default number of spaces used when rendering tab characters",
submenu: true,
},
},
"display.showTokenUsage": {
type: "boolean",
default: false,
ui: {
tab: "appearance",
label: "Show Token Usage",
description: "Show per-turn token usage on assistant messages",
},
},
showHardwareCursor: {
type: "boolean",
default: true, // will be computed based on platform if undefined
ui: { tab: "appearance", label: "Show Hardware Cursor", description: "Show terminal cursor for IME support" },
},
clearOnShrink: {
type: "boolean",
default: false,
ui: {
tab: "appearance",
label: "Clear on Shrink",
description: "Clear empty rows when content shrinks (may cause flicker)",
},
},
// ────────────────────────────────────────────────────────────────────────
// Model
// ────────────────────────────────────────────────────────────────────────
// Reasoning and prompts
defaultThinkingLevel: {
type: "enum",
values: THINKING_EFFORTS,
default: "high",
ui: {
tab: "model",
label: "Thinking Level",
description: "Reasoning depth for thinking-capable models",
submenu: true,
},
},
hideThinkingBlock: {
type: "boolean",
default: false,
ui: { tab: "model", label: "Hide Thinking Blocks", description: "Hide thinking blocks in assistant responses" },
},
repeatToolDescriptions: {
type: "boolean",
default: false,
ui: {
tab: "model",
label: "Repeat Tool Descriptions",
description: "Render full tool descriptions in the system prompt instead of a tool name list",
},
},
// Sampling
temperature: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Temperature",
description: "Sampling temperature (0 = deterministic, 1 = creative, -1 = provider default)",
submenu: true,
},
},
topP: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Top P",
description: "Nucleus sampling cutoff (0-1, -1 = provider default)",
submenu: true,
},
},
topK: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Top K",
description: "Sample from top-K tokens (-1 = provider default)",
submenu: true,
},
},
minP: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Min P",
description: "Minimum probability threshold (0-1, -1 = provider default)",
submenu: true,
},
},
presencePenalty: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Presence Penalty",
description: "Penalty for introducing already-present tokens (-1 = provider default)",
submenu: true,
},
},
repetitionPenalty: {
type: "number",
default: -1,
ui: {
tab: "model",
label: "Repetition Penalty",
description: "Penalty for repeated tokens (-1 = provider default)",
submenu: true,
},
},
serviceTier: {
type: "enum",
values: ["none", "auto", "default", "flex", "scale", "priority"] as const,
default: "none",
ui: {
tab: "model",
label: "Service Tier",
description: "OpenAI processing priority (none = omit parameter)",
submenu: true,
},
},
// Retries
"retry.enabled": { type: "boolean", default: true },
"retry.maxRetries": {
type: "number",
default: 3,
ui: {
tab: "model",
label: "Retry Attempts",
description: "Maximum retry attempts on API errors",
submenu: true,
},
},
"retry.baseDelayMs": { type: "number", default: 2000 },
"retry.fallbackChains": { type: "record", default: {} as Record<string, string[]> },
"retry.fallbackRevertPolicy": {
type: "enum",
values: ["cooldown-expiry", "never"] as const,
default: "cooldown-expiry",
ui: {
tab: "model",
label: "Fallback Revert Policy",
description: "When to return to the primary model after a fallback",
submenu: true,
},
},
// ────────────────────────────────────────────────────────────────────────
// Interaction
// ────────────────────────────────────────────────────────────────────────
// Conversation flow
steeringMode: {
type: "enum",
values: ["all", "one-at-a-time"] as const,
default: "one-at-a-time",
ui: {
tab: "interaction",
label: "Steering Mode",
description: "How to process queued messages while agent is working",
},
},
followUpMode: {
type: "enum",
values: ["all", "one-at-a-time"] as const,
default: "one-at-a-time",
ui: {
tab: "interaction",
label: "Follow-Up Mode",
description: "How to drain follow-up messages after a turn completes",
},
},
interruptMode: {
type: "enum",
values: ["immediate", "wait"] as const,
default: "immediate",
ui: {
tab: "interaction",
label: "Interrupt Mode",
description: "When steering messages interrupt tool execution",
},
},
// Input and startup
doubleEscapeAction: {
type: "enum",
values: ["branch", "tree", "none"] as const,
default: "tree",
ui: {
tab: "interaction",
label: "Double-Escape Action",
description: "Action when pressing Escape twice with empty editor",
},
},
treeFilterMode: {
type: "enum",
values: ["default", "no-tools", "user-only", "labeled-only", "all"] as const,
default: "default",
ui: {
tab: "interaction",
label: "Session Tree Filter",
description: "Default filter mode when opening the session tree",
},
},
autocompleteMaxVisible: {
type: "number",
default: 5,
ui: {
tab: "interaction",
label: "Autocomplete Items",
description: "Max visible items in autocomplete dropdown (3-20)",
submenu: true,
},
},
"startup.quiet": {
type: "boolean",
default: false,
ui: {
tab: "interaction",
label: "Quiet Startup",
description: "Skip welcome screen and startup status messages",
},
},
"startup.checkUpdate": {
type: "boolean",
default: true,
ui: {
tab: "interaction",
label: "Check for Updates",
description: "If false, skip update check",
},
},
collapseChangelog: {
type: "boolean",
default: false,
ui: { tab: "interaction", label: "Collapse Changelog", description: "Show condensed changelog after updates" },
},
// Notifications
"completion.notify": {
type: "enum",
values: ["on", "off"] as const,
default: "on",
ui: { tab: "interaction", label: "Completion Notification", description: "Notify when the agent completes" },
},
"ask.timeout": {
type: "number",
default: 30,
ui: {
tab: "interaction",
label: "Ask Timeout",
description: "Auto-select recommended option after timeout (0 to disable)",
submenu: true,
},
},
"ask.notify": {
type: "enum",
values: ["on", "off"] as const,
default: "on",
ui: { tab: "interaction", label: "Ask Notification", description: "Notify when ask tool is waiting for input" },
},
// Speech-to-text
"stt.enabled": {
type: "boolean",
default: false,
ui: { tab: "interaction", label: "Speech-to-Text", description: "Enable speech-to-text input via microphone" },
},
"stt.language": {
type: "string",
default: "en",
ui: {
tab: "interaction",
label: "Speech Language",
description: "Language code for transcription (e.g., en, es, fr)",
submenu: true,
},
},
"stt.modelName": {
type: "enum",
values: ["tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large"] as const,
default: "base.en",
ui: {
tab: "interaction",
label: "Speech Model",
description: "Whisper model size (larger = more accurate but slower)",
submenu: true,
},
},
// ────────────────────────────────────────────────────────────────────────
// Context
// ────────────────────────────────────────────────────────────────────────
// Context promotion
"contextPromotion.enabled": {
type: "boolean",
default: true,
ui: {
tab: "context",
label: "Auto-Promote Context",
description: "Promote to a larger-context model on context overflow instead of compacting",
},
},
// Compaction
"compaction.enabled": {
type: "boolean",
default: true,
ui: {
tab: "context",
label: "Auto-Compact",
description: "Automatically compact context when it gets too large",
},
},
"compaction.strategy": {
type: "enum",
values: ["context-full", "handoff", "off"] as const,
default: "context-full",
ui: {
tab: "context",
label: "Compaction Strategy",
description: "Choose in-place context-full maintenance, auto-handoff, or disable auto maintenance (off)",
submenu: true,
},
},
"compaction.thresholdPercent": {
type: "number",
default: -1,
ui: {
tab: "context",
label: "Compaction Threshold",
description: "Percent threshold for context maintenance; set to Default to use legacy reserve-based behavior",
submenu: true,
},
},
"compaction.thresholdTokens": {
type: "number",
default: -1,
ui: {
tab: "context",
label: "Compaction Token Limit",
description: "Fixed token limit for context maintenance; overrides percentage if set",
submenu: true,
},
},
"compaction.handoffSaveToDisk": {
type: "boolean",
default: false,
ui: {
tab: "context",
label: "Save Handoff Docs",
description: "Save generated handoff documents to markdown files for the auto-handoff flow",
},
},
"compaction.remoteEnabled": {
type: "boolean",
default: true,
ui: {
tab: "context",
label: "Remote Compaction",
description: "Use remote compaction endpoints when available instead of local summarization",
},
},
"compaction.reserveTokens": { type: "number", default: 16384 },
"compaction.keepRecentTokens": { type: "number", default: 20000 },
"compaction.autoContinue": { type: "boolean", default: true },
"compaction.remoteEndpoint": { type: "string", default: undefined },
// Idle compaction
"compaction.idleEnabled": {
type: "boolean",
default: false,
ui: {
tab: "context",
label: "Idle Compaction",
description: "Compact context while idle when token count exceeds threshold",
},
},
"compaction.idleThresholdTokens": {
type: "number",
default: 200000,
ui: {
tab: "context",
label: "Idle Compaction Threshold",
description: "Token count above which idle compaction triggers",
submenu: true,
},
},
"compaction.idleTimeoutSeconds": {
type: "number",
default: 300,
ui: {
tab: "context",
label: "Idle Compaction Delay",
description: "Seconds to wait while idle before compacting",
submenu: true,
},
},
// Branch summaries
"branchSummary.enabled": {
type: "boolean",
default: false,
ui: { tab: "context", label: "Branch Summaries", description: "Prompt to summarize when leaving a branch" },
},
"branchSummary.reserveTokens": { type: "number", default: 16384 },
// Memories
"memories.enabled": {
type: "boolean",
default: false,
ui: {
tab: "context",
label: "Memories",
description: "Enable autonomous memory extraction and consolidation",
},
},
"memories.maxRolloutsPerStartup": { type: "number", default: 64 },
"memories.maxRolloutAgeDays": { type: "number", default: 30 },
"memories.minRolloutIdleHours": { type: "number", default: 12 },
"memories.threadScanLimit": { type: "number", default: 300 },
"memories.maxRawMemoriesForGlobal": { type: "number", default: 200 },
"memories.stage1Concurrency": { type: "number", default: 8 },
"memories.stage1LeaseSeconds": { type: "number", default: 120 },
"memories.stage1RetryDelaySeconds": { type: "number", default: 120 },
"memories.phase2LeaseSeconds": { type: "number", default: 180 },
"memories.phase2RetryDelaySeconds": { type: "number", default: 180 },
"memories.phase2HeartbeatSeconds": { type: "number", default: 30 },
"memories.rolloutPayloadPercent": { type: "number", default: 0.7 },
"memories.fallbackTokenLimit": { type: "number", default: 16000 },
"memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
// TTSR
"ttsr.enabled": {
type: "boolean",
default: true,
ui: {
tab: "context",
label: "TTSR",
description: "Time Traveling Stream Rules: interrupt agent when output matches patterns",
},
},
"ttsr.contextMode": {
type: "enum",
values: ["discard", "keep"] as const,
default: "discard",
ui: {
tab: "context",
label: "TTSR Context Mode",
description: "What to do with partial output when TTSR triggers",
},
},
"ttsr.interruptMode": {
type: "enum",
values: ["never", "prose-only", "tool-only", "always"] as const,
default: "always",
ui: {
tab: "context",
label: "TTSR Interrupt Mode",
description: "When to interrupt mid-stream vs inject warning after completion",
submenu: true,
},
},
"ttsr.repeatMode": {
type: "enum",
values: ["once", "after-gap"] as const,
default: "once",
ui: {
tab: "context",
label: "TTSR Repeat Mode",
description: "How rules can repeat: once per session or after a message gap",
},
},
"ttsr.repeatGap": {
type: "number",
default: 10,
ui: {
tab: "context",
label: "TTSR Repeat Gap",
description: "Messages before a rule can trigger again",
submenu: true,
},
},
// ────────────────────────────────────────────────────────────────────────
// Editing
// ────────────────────────────────────────────────────────────────────────
// Edit tool
"edit.mode": {
type: "enum",
values: ["replace", "patch", "hashline", "chunk"] as const,
default: "hashline",
ui: {
tab: "editing",
label: "Edit Mode",
description: "Select the edit tool variant (replace, patch, hashline, or chunk)",
},
},
"edit.fuzzyMatch": {
type: "boolean",
default: true,
ui: {
tab: "editing",
label: "Fuzzy Match",
description: "Accept high-confidence fuzzy matches for whitespace differences",
},
},
"edit.fuzzyThreshold": {
type: "number",
default: 0.95,
ui: {
tab: "editing",
label: "Fuzzy Match Threshold",
description: "Similarity threshold for fuzzy matches",
submenu: true,
},
},
"edit.streamingAbort": {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Abort on Failed Preview",
description: "Abort streaming edit tool calls when patch preview fails",
},
},
"edit.blockAutoGenerated": {
type: "boolean",
default: true,
ui: {
tab: "editing",
label: "Block Auto-Generated Files",
description: "Prevent editing of files that appear to be auto-generated (protoc, sqlc, swagger, etc.)",
},
},
readLineNumbers: {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Line Numbers",
description: "Prepend line numbers to read tool output by default",
},
},
readHashLines: {
type: "boolean",
default: true,
ui: {
tab: "editing",
label: "Hash Lines",
description: "Include line hashes in read output for hashline edit mode (LINE#ID:content)",
},
},
"read.defaultLimit": {
type: "number",
default: 300,
ui: {
tab: "editing",
label: "Default Read Limit",
description: "Default number of lines returned when agent calls read without a limit",
submenu: true,
},
},
"read.prosechunks": {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Prose Chunks",
description: "Enable chunk rendering for prose files in chunk edit mode",
},
},
"read.explorechunks": {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Explore Chunks",
description: "Show chunk tree without checksums for read-only agents like explore",
},
},
"read.anchorstyle": {
type: "enum",
values: ["full", "kind", "bare"],
default: "full",
ui: {
tab: "editing",
label: "Anchor Style",
description: "Render chunk anchors with full names, kind prefixes, or checksum-only tags",
submenu: true,
},
},
// LSP
"lsp.enabled": {
type: "boolean",
default: true,
ui: { tab: "editing", label: "LSP", description: "Enable the lsp tool for language server protocol" },
},
"lsp.formatOnWrite": {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Format on Write",
description: "Automatically format code files using LSP after writing",
},
},
"lsp.diagnosticsOnWrite": {
type: "boolean",
default: true,
ui: {
tab: "editing",
label: "Diagnostics on Write",
description: "Return LSP diagnostics after writing code files",
},
},
"lsp.diagnosticsOnEdit": {
type: "boolean",
default: false,
ui: {
tab: "editing",
label: "Diagnostics on Edit",
description: "Return LSP diagnostics after editing code files",
},
},
// Bash interceptor
"bashInterceptor.enabled": {
type: "boolean",
default: false,
ui: { tab: "editing", label: "Bash Interceptor", description: "Block shell commands that have dedicated tools" },
},
"bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
// Python
"python.toolMode": {
type: "enum",
values: ["ipy-only", "bash-only", "both"] as const,
default: "both",
ui: { tab: "editing", label: "Python Tool Mode", description: "How Python code is executed" },
},
"python.kernelMode": {
type: "enum",
values: ["session", "per-call"] as const,
default: "session",
ui: {
tab: "editing",
label: "Python Kernel Mode",
description: "Whether to keep IPython kernel alive across calls",
},
},
"python.sharedGateway": {
type: "boolean",
default: true,
ui: {
tab: "editing",
label: "Shared Python Gateway",
description: "Share IPython kernel gateway across pi instances",
},
},
// ────────────────────────────────────────────────────────────────────────
// Tools
// ────────────────────────────────────────────────────────────────────────
// Todo tool
"todo.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Todos", description: "Enable the todo_write tool for task tracking" },
},
"todo.reminders": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Todo Reminders", description: "Remind agent to complete todos before stopping" },
},
"todo.reminders.max": {
type: "number",
default: 3,
ui: {
tab: "tools",
label: "Todo Reminder Limit",
description: "Maximum reminders to complete todos before giving up",
submenu: true,
},
},
"todo.eager": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Create Todos Automatically",
description: "Automatically create a comprehensive todo list after the first message",
},
},
// Search and AST tools
"find.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Find", description: "Enable the find tool for file searching" },
},
"grep.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Grep", description: "Enable the grep tool for content searching" },
},
"grep.contextBefore": {
type: "number",
default: 0,
ui: {
tab: "tools",
label: "Grep Context Before",
description: "Lines of context before each grep match",
submenu: true,
},
},
"grep.contextAfter": {
type: "number",
default: 0,
ui: {
tab: "tools",
label: "Grep Context After",
description: "Lines of context after each grep match",
submenu: true,
},
},
"astGrep.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "AST Grep",
description: "Enable the ast_grep tool for structural AST search",
},
},
"astEdit.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "AST Edit",
description: "Enable the ast_edit tool for structural AST rewrites",
},
},
// Optional tools
"notebook.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Notebook", description: "Enable the notebook tool for notebook editing" },
},
"renderMermaid.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Render Mermaid",
description: "Enable the render_mermaid tool for Mermaid-to-ASCII rendering",
},
},
"debug.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "Debug",
description: "Enable the debug tool for DAP-based debugging",
},
},
"calc.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Calculator",
description: "Enable the calculator tool for basic calculations",
},
},
"inspect_image.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Inspect Image",
description: "Enable the inspect_image tool, delegating image understanding to a vision-capable model",
},
},
"checkpoint.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Checkpoint/Rewind",
description: "Enable the checkpoint and rewind tools for context checkpointing",
},
},
// Fetching and browser
"fetch.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Read URLs", description: "Allow the read tool to fetch and process URLs" },
},
"github.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "GitHub CLI",
description: "Enable read-only gh_* tools for GitHub repository, issue, pull request, diff, and search access",
},
},
"web_search.enabled": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "Web Search", description: "Enable the web_search tool for web searching" },
},
"browser.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "Browser",
description: "Enable the browser tool (Ulixee Hero)",
},
},
"browser.headless": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "Headless Browser",
description: "Launch browser in headless mode (disable to show browser UI)",
},
},
"browser.screenshotDir": {
type: "string",
default: undefined,
ui: {
tab: "tools",
label: "Screenshot directory",
description:
"Directory to save screenshots. If unset, screenshots go to a temp file. Supports ~. Examples: ~/Downloads, ~/Desktop, /sdcard/Download (Android)",
},
},
// Tool execution
"tools.intentTracing": {
type: "boolean",
default: true,
ui: {
tab: "tools",
label: "Intent Tracing",
description: "Ask the agent to describe the intent of each tool call before executing it",
},
},
"tools.maxTimeout": {
type: "number",
default: 0,
ui: {
tab: "tools",
label: "Max Tool Timeout",
description: "Maximum timeout in seconds the agent can set for any tool (0 = no limit)",
submenu: true,
},
},
// Async jobs
"async.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Async Execution",
description: "Enable async bash commands and background task execution",
},
},
"async.maxJobs": {
type: "number",
default: 100,
ui: {
tab: "tools",
label: "Max Async Jobs",
description: "Maximum concurrent background jobs (1-100)",
submenu: true,
},
},
// MCP
"mcp.enableProjectConfig": {
type: "boolean",
default: true,
ui: { tab: "tools", label: "MCP Project Config", description: "Load .mcp.json/mcp.json from project root" },
},
"mcp.discoveryMode": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "MCP Tool Discovery",
description: "Hide MCP tools by default and expose them through a tool discovery tool",
},
},
"mcp.discoveryDefaultServers": {
type: "array",
default: [] as string[],
ui: {
tab: "tools",
label: "MCP Discovery Default Servers",
description: "Keep MCP tools from these servers visible while discovery mode hides other MCP tools",
},
},
"mcp.notifications": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "MCP Update Injection",
description: "Inject MCP resource updates into the agent conversation",
},
},
"mcp.notificationDebounceMs": {
type: "number",
default: 500,
ui: {
tab: "tools",
label: "MCP Notification Debounce",
description: "Debounce window for MCP resource update notifications before injecting into conversation",
},
},
// ────────────────────────────────────────────────────────────────────────
// Tasks
// ────────────────────────────────────────────────────────────────────────
// Delegation
"task.isolation.mode": {
type: "enum",
values: ["none", "worktree", "fuse-overlay", "fuse-projfs"] as const,
default: "none",
ui: {
tab: "tasks",
label: "Isolation Mode",
description:
"Isolation mode for subagents (none, git worktree, fuse-overlayfs on Unix, or ProjFS on Windows via fuse-projfs; unsupported modes fall back to worktree)",
submenu: true,
},
},
"task.isolation.merge": {
type: "enum",
values: ["patch", "branch"] as const,
default: "patch",
ui: {
tab: "tasks",
label: "Isolation Merge Strategy",
description: "How isolated task changes are integrated (patch apply or branch merge)",
submenu: true,
},
},
"task.isolation.commits": {
type: "enum",
values: ["generic", "ai"] as const,
default: "generic",
ui: {
tab: "tasks",
label: "Isolation Commit Style",
description: "Commit message style for nested repo changes (generic or AI-generated)",
submenu: true,
},
},
"task.eager": {
type: "boolean",
default: false,
ui: {
tab: "tasks",
label: "Prefer Task Delegation",
description: "Encourage the agent to delegate work to subagents unless changes are trivial",
},
},
"task.maxConcurrency": {
type: "number",
default: 32,
ui: {
tab: "tasks",
label: "Max Concurrent Tasks",
description: "Concurrent limit for subagents",
submenu: true,
},
},
"task.maxRecursionDepth": {
type: "number",
default: 2,
ui: {
tab: "tasks",
label: "Max Task Recursion",
description: "How many levels deep subagents can spawn their own subagents",
submenu: true,
},
},
"task.disabledAgents": {
type: "array",
default: [] as string[],
},
"task.agentModelOverrides": {
type: "record",
default: {} as Record<string, string>,
},
"tasks.todoClearDelay": {
type: "number",
default: 60,
ui: {
tab: "tasks",
label: "Todo auto-clear delay",
description: "How long to wait before removing completed/abandoned tasks from the list",
submenu: true,
},
},
// Skills
"skills.enabled": { type: "boolean", default: true },
"skills.enableSkillCommands": {
type: "boolean",
default: true,
ui: { tab: "tasks", label: "Skill Commands", description: "Register skills as /skill:name commands" },
},
"skills.enableCodexUser": { type: "boolean", default: true },
"skills.enableClaudeUser": { type: "boolean", default: true },
"skills.enableClaudeProject": { type: "boolean", default: true },
"skills.enablePiUser": { type: "boolean", default: true },
"skills.enablePiProject": { type: "boolean", default: true },
"skills.customDirectories": { type: "array", default: [] as string[] },
"skills.ignoredSkills": { type: "array", default: [] as string[] },
"skills.includeSkills": { type: "array", default: [] as string[] },
// Commands
"commands.enableClaudeUser": {
type: "boolean",
default: true,
ui: { tab: "tasks", label: "Claude User Commands", description: "Load commands from ~/.claude/commands/" },
},
"commands.enableClaudeProject": {
type: "boolean",
default: true,
ui: { tab: "tasks", label: "Claude Project Commands", description: "Load commands from .claude/commands/" },
},
// ────────────────────────────────────────────────────────────────────────
// Providers
// ────────────────────────────────────────────────────────────────────────
// Secret handling
"secrets.enabled": {
type: "boolean",
default: false,
ui: { tab: "providers", label: "Hide Secrets", description: "Obfuscate secrets before sending to AI providers" },
},
// Provider selection
"providers.webSearch": {
type: "enum",
values: [
"auto",
"exa",
"brave",
"jina",
"kimi",
"zai",
"perplexity",
"anthropic",
"gemini",
"codex",
"tavily",
"kagi",
"synthetic",
"parallel",
] as const,
default: "auto",
ui: {
tab: "providers",
label: "Web Search Provider",
description: "Provider for web search tool",
submenu: true,
},
},
"providers.image": {
type: "enum",
values: ["auto", "gemini", "openrouter"] as const,
default: "auto",
ui: {
tab: "providers",
label: "Image Provider",
description: "Provider for image generation tool",
submenu: true,
},
},
"providers.kimiApiFormat": {
type: "enum",
values: ["openai", "anthropic"] as const,
default: "anthropic",
ui: {
tab: "providers",
label: "Kimi API Format",
description: "API format for Kimi Code provider",
submenu: true,
},
},
"providers.openaiWebsockets": {
type: "enum",
values: ["auto", "off", "on"] as const,
default: "auto",
ui: {
tab: "providers",
label: "OpenAI WebSockets",
description: "Websocket policy for OpenAI Codex models (auto uses model defaults, on forces, off disables)",
submenu: true,
},
},
"providers.parallelFetch": {
type: "boolean",
default: true,
ui: {
tab: "providers",
label: "Parallel Fetch",
description: "Use Parallel extract API for URL fetching when credentials are available",
},
},
// Exa
"exa.enabled": {
type: "boolean",
default: true,
ui: { tab: "providers", label: "Exa", description: "Master toggle for all Exa search tools" },
},
"exa.enableSearch": {
type: "boolean",
default: true,
ui: { tab: "providers", label: "Exa Search", description: "Basic search, deep search, code search, crawl" },
},
"exa.enableResearcher": {
type: "boolean",
default: false,
ui: { tab: "providers", label: "Exa Researcher", description: "AI-powered deep research tasks" },
},
"exa.enableWebsets": {
type: "boolean",
default: false,
ui: { tab: "providers", label: "Exa Websets", description: "Webset management and enrichment tools" },
},
"commit.mapReduceEnabled": { type: "boolean", default: true },
"commit.mapReduceMinFiles": { type: "number", default: 4 },
"commit.mapReduceMaxFileTokens": { type: "number", default: 50000 },
"commit.mapReduceTimeoutMs": { type: "number", default: 120000 },
"commit.mapReduceMaxConcurrency": { type: "number", default: 5 },
"commit.changelogMaxDiffChars": { type: "number", default: 120000 },
"dev.autoqa": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Auto QA",
description: "Enable automated tool issue reporting (report_tool_issue) for all agents",
},
},
"thinkingBudgets.minimal": { type: "number", default: 1024 },
"thinkingBudgets.low": { type: "number", default: 2048 },
"thinkingBudgets.medium": { type: "number", default: 8192 },
"thinkingBudgets.high": { type: "number", default: 16384 },
"thinkingBudgets.xhigh": { type: "number", default: 32768 },
} as const;
// ═══════════════════════════════════════════════════════════════════════════
// Type Inference
// ═══════════════════════════════════════════════════════════════════════════
type Schema = typeof SETTINGS_SCHEMA;
/** All valid setting paths */
export type SettingPath = keyof Schema;
/** Infer the value type for a setting path */
export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean" }
? boolean
: Schema[P] extends { type: "string" }
? string | undefined
: Schema[P] extends { type: "number" }
? number
: Schema[P] extends { type: "enum"; values: infer V }
? V extends readonly string[]
? V[number]
: never
: Schema[P] extends { type: "array"; default: infer D }
? D
: Schema[P] extends { type: "record"; default: infer D }
? D
: never;
/** Get the default value for a setting path */
export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
return SETTINGS_SCHEMA[path].default as SettingValue<P>;
}
/** Check if a path has UI metadata (should appear in settings panel) */
export function hasUi(path: SettingPath): boolean {
return "ui" in SETTINGS_SCHEMA[path];
}
/** Get UI metadata for a path (undefined if no UI) */
export function getUi(path: SettingPath): UiMetadata | undefined {
const def = SETTINGS_SCHEMA[path];
return "ui" in def ? (def.ui as UiMetadata) : undefined;
}
/** Get all paths for a specific tab */
export function getPathsForTab(tab: SettingTab): SettingPath[] {
return (Object.keys(SETTINGS_SCHEMA) as SettingPath[]).filter(path => {
const ui = getUi(path);
return ui?.tab === tab;
});
}
/** Get the type of a setting */
export function getType(path: SettingPath): SettingDef["type"] {
return SETTINGS_SCHEMA[path].type;
}
/** Get enum values for an enum setting */
export function getEnumValues(path: SettingPath): readonly string[] | undefined {
const def = SETTINGS_SCHEMA[path];
return "values" in def ? (def.values as readonly string[]) : undefined;
}
// ═══════════════════════════════════════════════════════════════════════════
// Derived Types from Schema
// ═══════════════════════════════════════════════════════════════════════════
/** Status line preset - derived from schema */
export type StatusLinePreset = SettingValue<"statusLine.preset">;
/** Status line separator style - derived from schema */
export type StatusLineSeparatorStyle = SettingValue<"statusLine.separator">;
/** Tree selector filter mode - derived from schema */
export type TreeFilterMode = SettingValue<"treeFilterMode">;
// ═══════════════════════════════════════════════════════════════════════════
// Typed Group Definitions
// ═══════════════════════════════════════════════════════════════════════════
export interface CompactionSettings {
enabled: boolean;
strategy: "context-full" | "handoff" | "off";
thresholdPercent: number;
thresholdTokens: number;
reserveTokens: number;
keepRecentTokens: number;
handoffSaveToDisk: boolean;
autoContinue: boolean;
remoteEnabled: boolean;
remoteEndpoint: string | undefined;
idleEnabled: boolean;
idleThresholdTokens: number;
idleTimeoutSeconds: number;
}
export interface ContextPromotionSettings {
enabled: boolean;
}
export interface RetrySettings {
enabled: boolean;
maxRetries: number;
baseDelayMs: number;
}
export interface MemoriesSettings {
enabled: boolean;
maxRolloutsPerStartup: number;
maxRolloutAgeDays: number;
minRolloutIdleHours: number;
threadScanLimit: number;
maxRawMemoriesForGlobal: number;
stage1Concurrency: number;
stage1LeaseSeconds: number;
stage1RetryDelaySeconds: number;
phase2LeaseSeconds: number;
phase2RetryDelaySeconds: number;
phase2HeartbeatSeconds: number;
rolloutPayloadPercent: number;
fallbackTokenLimit: number;
summaryInjectionTokenLimit: number;
}
export interface TodoCompletionSettings {
enabled: boolean;
maxReminders: number;
}
export interface BranchSummarySettings {
enabled: boolean;
reserveTokens: number;
}
export interface SkillsSettings {
enabled?: boolean;
enableSkillCommands?: boolean;
enableCodexUser?: boolean;
enableClaudeUser?: boolean;
enableClaudeProject?: boolean;
enablePiUser?: boolean;
enablePiProject?: boolean;
customDirectories?: string[];
ignoredSkills?: string[];
includeSkills?: string[];
disabledExtensions?: string[];
}
export interface CommitSettings {
mapReduceEnabled: boolean;
mapReduceMinFiles: number;
mapReduceMaxFileTokens: number;
mapReduceTimeoutMs: number;
mapReduceMaxConcurrency: number;
changelogMaxDiffChars: number;
}
export interface TtsrSettings {
enabled: boolean;
contextMode: "discard" | "keep";
interruptMode: "never" | "prose-only" | "tool-only" | "always";
repeatMode: "once" | "after-gap";
repeatGap: number;
}
export interface ExaSettings {
enabled: boolean;
enableSearch: boolean;
enableResearcher: boolean;
enableWebsets: boolean;
}
export interface StatusLineSettings {
preset: StatusLinePreset;
separator: StatusLineSeparatorStyle;
showHookStatus: boolean;
leftSegments: StatusLineSegmentId[];
rightSegments: StatusLineSegmentId[];
segmentOptions: Record<string, unknown>;
}
export interface ThinkingBudgetsSettings {
minimal: number;
low: number;
medium: number;
high: number;
xhigh: number;
}
export interface SttSettings {
enabled: boolean;
language: string | undefined;
modelName: string;
whisperPath: string | undefined;
modelPath: string | undefined;
}
export interface BashInterceptorRule {
pattern: string;
flags?: string;
tool: string;
message: string;
allowSubcommands?: string[];
}
/** Map group prefix -> typed settings interface */
export interface GroupTypeMap {
compaction: CompactionSettings;
contextPromotion: ContextPromotionSettings;
retry: RetrySettings;
memories: MemoriesSettings;
branchSummary: BranchSummarySettings;
skills: SkillsSettings;
commit: CommitSettings;
ttsr: TtsrSettings;
exa: ExaSettings;
statusLine: StatusLineSettings;
thinkingBudgets: ThinkingBudgetsSettings;
stt: SttSettings;
modelRoles: Record<string, string>;
modelTags: ModelTagsSettings;
cycleOrder: string[];
}
export type GroupPrefix = keyof GroupTypeMap;