diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index e7d94d88..5a999392 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -48,6 +48,20 @@ Edit directly or use `/settings` for common options. | `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) | | `showHardwareCursor` | boolean | `false` | Show terminal cursor | +### Warnings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `warnings.anthropicExtraUsage` | boolean | `true` | Show a warning when Anthropic subscription auth may use paid extra usage | + +```json +{ + "warnings": { + "anthropicExtraUsage": false + } +} +``` + ### Compaction | Setting | Type | Default | Description | @@ -226,6 +240,9 @@ See [packages.md](packages.md) for package management details. "maxRetries": 3 }, "enabledModels": ["claude-*", "gpt-4o"], + "warnings": { + "anthropicExtraUsage": true + }, "packages": ["pi-skills"] } ``` diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index da351d2f..e82defe2 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -52,6 +52,10 @@ export interface MarkdownSettings { codeBlockIndent?: string; // default: " " } +export interface WarningSettings { + anthropicExtraUsage?: boolean; // default: true +} + export type TransportSetting = Transport; /** @@ -104,6 +108,7 @@ export interface Settings { autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME markdown?: MarkdownSettings; + warnings?: WarningSettings; sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag) } @@ -1049,4 +1054,14 @@ export class SettingsManager { getCodeBlockIndent(): string { return this.settings.markdown?.codeBlockIndent ?? " "; } + + getWarnings(): WarningSettings { + return { ...(this.settings.warnings ?? {}) }; + } + + setWarnings(warnings: WarningSettings): void { + this.globalSettings.warnings = { ...warnings }; + this.markModified("warnings"); + this.save(); + } } diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index e56bcf54..0bc92052 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -11,6 +11,7 @@ import { Spacer, Text, } from "@mariozechner/pi-tui"; +import type { WarningSettings } from "../../../core/settings-manager.js"; import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -53,6 +54,7 @@ export interface SettingsConfig { quietStartup: boolean; clearOnShrink: boolean; showTerminalProgress: boolean; + warnings: WarningSettings; } export interface SettingsCallbacks { @@ -79,12 +81,55 @@ export interface SettingsCallbacks { onQuietStartupChange: (enabled: boolean) => void; onClearOnShrinkChange: (enabled: boolean) => void; onShowTerminalProgressChange: (enabled: boolean) => void; + onWarningsChange: (warnings: WarningSettings) => void; onCancel: () => void; } /** * A submenu component for selecting from a list of options. */ +class WarningSettingsSubmenu extends Container { + private settingsList: SettingsList; + private state: WarningSettings; + + constructor(warnings: WarningSettings, onChange: (warnings: WarningSettings) => void, onCancel: () => void) { + super(); + + this.state = { ...warnings }; + + const items: SettingItem[] = [ + { + id: "anthropic-extra-usage", + label: "Anthropic extra usage", + description: "Warn when Anthropic subscription auth may use paid extra usage", + currentValue: (this.state.anthropicExtraUsage ?? true) ? "true" : "false", + values: ["true", "false"], + }, + ]; + + this.settingsList = new SettingsList( + items, + Math.min(items.length, 10), + getSettingsListTheme(), + (id, newValue) => { + switch (id) { + case "anthropic-extra-usage": + this.state = { ...this.state, anthropicExtraUsage: newValue === "true" }; + onChange({ ...this.state }); + break; + } + }, + onCancel, + ); + + this.addChild(this.settingsList); + } + + handleInput(data: string): void { + this.settingsList.handleInput(data); + } +} + class SelectSubmenu extends Container { private selectList: SelectList; @@ -159,6 +204,7 @@ export class SettingsSelectorComponent extends Container { super(); const supportsImages = getCapabilities().images; + let currentWarnings = { ...config.warnings }; const items: SettingItem[] = [ { @@ -233,6 +279,21 @@ export class SettingsSelectorComponent extends Container { currentValue: config.treeFilterMode, values: ["default", "no-tools", "user-only", "labeled-only", "all"], }, + { + id: "warnings", + label: "Warnings", + description: "Enable or disable individual warnings", + currentValue: "configure", + submenu: (_currentValue, done) => + new WarningSettingsSubmenu( + currentWarnings, + (warnings) => { + currentWarnings = warnings; + callbacks.onWarningsChange(warnings); + }, + () => done(), + ), + }, { id: "thinking", label: "Thinking level", diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 2436c7dc..e29c4408 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3788,6 +3788,7 @@ export class InteractiveMode { quietStartup: this.settingsManager.getQuietStartup(), clearOnShrink: this.settingsManager.getClearOnShrink(), showTerminalProgress: this.settingsManager.getShowTerminalProgress(), + warnings: this.settingsManager.getWarnings(), }, { onAutoCompactChange: (enabled) => { @@ -3901,6 +3902,9 @@ export class InteractiveMode { onShowTerminalProgressChange: (enabled) => { this.settingsManager.setShowTerminalProgress(enabled); }, + onWarningsChange: (warnings) => { + this.settingsManager.setWarnings(warnings); + }, onCancel: () => { done(); this.ui.requestRender(); @@ -3963,6 +3967,9 @@ export class InteractiveMode { private async maybeWarnAboutAnthropicSubscriptionAuth( model: Model | undefined = this.session.model, ): Promise { + if (this.settingsManager.getWarnings().anthropicExtraUsage === false) { + return; + } if (this.anthropicSubscriptionWarningShown) { return; } diff --git a/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts b/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts index e934d67e..16ca930a 100644 --- a/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts +++ b/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts @@ -1,10 +1,17 @@ import { describe, expect, test, vi } from "vitest"; import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +function createSettingsManager(warnings: { anthropicExtraUsage?: boolean } = {}) { + return { + getWarnings: vi.fn().mockReturnValue(warnings), + }; +} + describe("InteractiveMode.maybeWarnAboutAnthropicSubscriptionAuth", () => { test("warns once when Anthropic subscription auth is detected", async () => { const fakeThis: any = { anthropicSubscriptionWarningShown: false, + settingsManager: createSettingsManager(), session: { modelRegistry: { authStorage: { @@ -30,6 +37,7 @@ describe("InteractiveMode.maybeWarnAboutAnthropicSubscriptionAuth", () => { test("warns when Anthropic OAuth is stored even if token refresh lookup would fail", async () => { const fakeThis: any = { anthropicSubscriptionWarningShown: false, + settingsManager: createSettingsManager(), session: { modelRegistry: { authStorage: { @@ -52,6 +60,7 @@ describe("InteractiveMode.maybeWarnAboutAnthropicSubscriptionAuth", () => { test("does not warn for non-Anthropic models", async () => { const fakeThis: any = { anthropicSubscriptionWarningShown: false, + settingsManager: createSettingsManager(), session: { modelRegistry: { authStorage: { @@ -70,4 +79,28 @@ describe("InteractiveMode.maybeWarnAboutAnthropicSubscriptionAuth", () => { expect(fakeThis.showWarning).not.toHaveBeenCalled(); expect(fakeThis.session.modelRegistry.getApiKeyForProvider).not.toHaveBeenCalled(); }); + + test("does not warn when Anthropic extra usage warning is disabled", async () => { + const fakeThis: any = { + anthropicSubscriptionWarningShown: false, + settingsManager: createSettingsManager({ anthropicExtraUsage: false }), + session: { + modelRegistry: { + authStorage: { + get: vi.fn(), + }, + getApiKeyForProvider: vi.fn(), + }, + }, + showWarning: vi.fn(), + }; + + await (InteractiveMode as any).prototype.maybeWarnAboutAnthropicSubscriptionAuth.call(fakeThis, { + provider: "anthropic", + }); + + expect(fakeThis.showWarning).not.toHaveBeenCalled(); + expect(fakeThis.session.modelRegistry.authStorage.get).not.toHaveBeenCalled(); + expect(fakeThis.session.modelRegistry.getApiKeyForProvider).not.toHaveBeenCalled(); + }); });