fix(config): change no_tools_mode default to false for pi_embedded
The previous default of true was overly restrictive. false is the correct default — tool-like prompts fall through to native handling only when explicitly enabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
# `/login` Auth Mode Switching Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Extend the `/login <provider> mode <value>` slash command to set `auth_mode` (api_key/oauth/auto) in `config.yaml` for all model tiers using a given provider, with a warning for providers that don't support auth_mode.
|
||||
|
||||
**Architecture:** Add a `mode` field to the `login` command type; extend `parseCommand` to recognise `/login <provider> mode <value>`; add a standalone `setProviderAuthMode()` helper in `minimal.ts` that updates all matching tiers via `persistConfig`; thread `configPath` + `config` through `MinimalTuiConfig` so the handler can persist; skip the auth_mode prompt for providers not in `AUTH_MODE_PROVIDERS`.
|
||||
|
||||
**Tech Stack:** TypeScript, Vitest, `yaml` (via `persistConfig`), existing `src/config/persistence.ts`, `src/frontends/tui/commands.ts`, `src/frontends/tui/minimal.ts`, `src/cli/tui.ts`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Extend the `login` command type and parser
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/commands.ts:18` (Command union type)
|
||||
- Modify: `src/frontends/tui/commands.ts:178-182` (parseCommand login branch)
|
||||
- Modify: `src/frontends/tui/commands.ts:240` (help text)
|
||||
- Modify: `src/frontends/tui/commands.ts:289` (SLASH_COMMANDS list — no change needed)
|
||||
- Modify: `src/frontends/tui/commands.ts:321` (COMMAND_TOOLTIPS)
|
||||
- Test: `src/frontends/tui/commands.test.ts`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add to `commands.test.ts` inside the existing `describe('parseCommand', ...)` block:
|
||||
|
||||
```typescript
|
||||
it('parses /login with mode subcommand', () => {
|
||||
expect(parseCommand('/login anthropic mode oauth')).toEqual({
|
||||
type: 'login', provider: 'anthropic', mode: 'oauth',
|
||||
});
|
||||
expect(parseCommand('/login openai mode api_key')).toEqual({
|
||||
type: 'login', provider: 'openai', mode: 'api_key',
|
||||
});
|
||||
expect(parseCommand('/login anthropic mode auto')).toEqual({
|
||||
type: 'login', provider: 'anthropic', mode: 'auto',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses /login without mode unchanged', () => {
|
||||
expect(parseCommand('/login')).toEqual({ type: 'login' });
|
||||
expect(parseCommand('/login anthropic')).toEqual({ type: 'login', provider: 'anthropic' });
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/commands.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL — `mode` property not present on result.
|
||||
|
||||
**Step 3: Update the Command union type**
|
||||
|
||||
In `commands.ts` line 18, change:
|
||||
```typescript
|
||||
| { type: 'login'; provider?: string }
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
| { type: 'login'; provider?: string; mode?: 'api_key' | 'oauth' | 'auto' }
|
||||
```
|
||||
|
||||
**Step 4: Update parseCommand**
|
||||
|
||||
Replace the existing login block (lines ~177–183):
|
||||
```typescript
|
||||
// Login
|
||||
if (trimmed === '/login') {
|
||||
return { type: 'login' };
|
||||
}
|
||||
if (trimmed.startsWith('/login ')) {
|
||||
const provider = trimmed.slice('/login '.length).trim();
|
||||
return { type: 'login', provider: provider || undefined };
|
||||
}
|
||||
```
|
||||
with:
|
||||
```typescript
|
||||
// Login
|
||||
if (trimmed === '/login') {
|
||||
return { type: 'login' };
|
||||
}
|
||||
if (trimmed.startsWith('/login ')) {
|
||||
const rest = trimmed.slice('/login '.length).trim();
|
||||
// /login <provider> mode <value>
|
||||
const modeMatch = rest.match(/^(\S+)\s+mode\s+(\S+)$/);
|
||||
if (modeMatch) {
|
||||
const modeValue = modeMatch[2].toLowerCase();
|
||||
if (modeValue === 'api_key' || modeValue === 'oauth' || modeValue === 'auto') {
|
||||
return { type: 'login', provider: modeMatch[1] || undefined, mode: modeValue };
|
||||
}
|
||||
}
|
||||
return { type: 'login', provider: rest || undefined };
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Update help text**
|
||||
|
||||
In `getHelpText()`, update the `/login` line to:
|
||||
```
|
||||
/login [provider] Authenticate (github, openai, anthropic, zai)
|
||||
/login <p> mode <m> Set auth mode for provider (api_key|oauth|auto)
|
||||
```
|
||||
|
||||
Update `COMMAND_TOOLTIPS['/login']` to:
|
||||
```typescript
|
||||
'/login': 'Authenticate with provider; use "mode api_key|oauth|auto" to switch auth mode',
|
||||
```
|
||||
|
||||
**Step 6: Run tests to verify they pass**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/commands.test.ts
|
||||
```
|
||||
|
||||
Expected: all PASS.
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/commands.ts src/frontends/tui/commands.test.ts
|
||||
git commit -m "feat(tui): extend /login parser to accept mode subcommand"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Thread `configPath` + `config` through `MinimalTuiConfig`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/minimal.ts:63-81` (MinimalTuiConfig interface)
|
||||
- Modify: `src/cli/tui.ts:437-458` (MinimalTui constructor call)
|
||||
|
||||
No new tests needed — this is plumbing only. Existing tests cover no regression.
|
||||
|
||||
**Step 1: Add fields to `MinimalTuiConfig`**
|
||||
|
||||
In `minimal.ts`, import `Config` and `persistConfig` at the top. The file already imports from `../../config/index.js` — add `Config` to that import and add a new import for `persistConfig`:
|
||||
|
||||
```typescript
|
||||
import type { Config, ModelConfig, ModelProvider } from '../../config/index.js';
|
||||
import { persistConfig } from '../../config/persistence.js';
|
||||
```
|
||||
|
||||
Then in `MinimalTuiConfig` add:
|
||||
```typescript
|
||||
configPath?: string;
|
||||
currentConfig?: Config;
|
||||
```
|
||||
|
||||
**Step 2: Pass `configPath` and `config` when constructing `MinimalTui` in `tui.ts`**
|
||||
|
||||
In `tui.ts` line ~437, add two properties to the constructor object:
|
||||
```typescript
|
||||
configPath,
|
||||
currentConfig: config,
|
||||
```
|
||||
|
||||
**Step 3: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/minimal.ts src/cli/tui.ts
|
||||
git commit -m "feat(tui): thread configPath and currentConfig into MinimalTuiConfig"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Implement `setProviderAuthMode` and wire into the login handler
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/minimal.ts` (new helper + updated handler)
|
||||
- Test: `src/frontends/tui/minimal.test.ts`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
`minimal.test.ts` tests the TUI at a higher level. Add a focused unit test for the new helper by extracting it. For now, add to `minimal.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock persistConfig so we can assert it was called correctly
|
||||
const { mockPersistConfig } = vi.hoisted(() => ({ mockPersistConfig: vi.fn() }));
|
||||
vi.mock('../../config/persistence.js', () => ({ persistConfig: mockPersistConfig }));
|
||||
|
||||
describe('applyAuthModeToConfig', () => {
|
||||
it('sets auth_mode on all tiers whose provider matches', () => {
|
||||
const config = {
|
||||
models: {
|
||||
default: { provider: 'anthropic', model: 'claude-sonnet-4' },
|
||||
fast: { provider: 'openai', model: 'gpt-4o-mini' },
|
||||
complex: { provider: 'anthropic', model: 'claude-opus-4' },
|
||||
},
|
||||
} as unknown as import('../../config/schema.js').Config;
|
||||
|
||||
const updated = applyAuthModeToConfig(config, 'anthropic', 'oauth');
|
||||
|
||||
expect(updated.models.default.auth_mode).toBe('oauth');
|
||||
expect(updated.models.complex.auth_mode).toBe('oauth');
|
||||
// openai tier must be untouched
|
||||
expect((updated.models.fast as { auth_mode?: string }).auth_mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates local_providers entries that match', () => {
|
||||
const config = {
|
||||
models: {
|
||||
default: { provider: 'openai', model: 'gpt-4o' },
|
||||
local_providers: {
|
||||
myAnthropic: { provider: 'anthropic', model: 'claude-haiku' },
|
||||
},
|
||||
},
|
||||
} as unknown as import('../../config/schema.js').Config;
|
||||
|
||||
const updated = applyAuthModeToConfig(config, 'anthropic', 'api_key');
|
||||
|
||||
expect(updated.models.local_providers!['myAnthropic'].auth_mode).toBe('api_key');
|
||||
expect((updated.models.default as { auth_mode?: string }).auth_mode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note: `applyAuthModeToConfig` must be exported from `minimal.ts` for test access.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/minimal.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL — `applyAuthModeToConfig` is not exported.
|
||||
|
||||
**Step 3: Implement `applyAuthModeToConfig` and `AUTH_MODE_PROVIDERS`**
|
||||
|
||||
Add near the top of `minimal.ts` (after imports):
|
||||
|
||||
```typescript
|
||||
/** Providers that honour auth_mode at runtime. All others get a warning. */
|
||||
export const AUTH_MODE_PROVIDERS: ReadonlySet<string> = new Set(['anthropic', 'openai']);
|
||||
|
||||
/**
|
||||
* Return a new Config with auth_mode set on every model tier whose provider
|
||||
* matches targetProvider. Does not mutate the original.
|
||||
*/
|
||||
export function applyAuthModeToConfig(
|
||||
config: Config,
|
||||
targetProvider: string,
|
||||
mode: 'api_key' | 'oauth' | 'auto',
|
||||
): Config {
|
||||
const applyToTier = (tier: ModelConfig): ModelConfig =>
|
||||
tier.provider === targetProvider ? { ...tier, auth_mode: mode } : tier;
|
||||
|
||||
const updatedModels = { ...config.models };
|
||||
|
||||
if (updatedModels.default) {
|
||||
updatedModels.default = applyToTier(updatedModels.default);
|
||||
}
|
||||
for (const key of ['fast', 'complex', 'local'] as const) {
|
||||
const tier = updatedModels[key];
|
||||
if (tier) {
|
||||
updatedModels[key] = applyToTier(tier);
|
||||
}
|
||||
}
|
||||
if (updatedModels.local_providers) {
|
||||
const updatedLocalProviders: Record<string, ModelConfig> = {};
|
||||
for (const [name, tier] of Object.entries(updatedModels.local_providers)) {
|
||||
updatedLocalProviders[name] = applyToTier(tier);
|
||||
}
|
||||
updatedModels.local_providers = updatedLocalProviders;
|
||||
}
|
||||
|
||||
return { ...config, models: updatedModels };
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/minimal.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Wire into `handleLoginCommand`**
|
||||
|
||||
Update the switch dispatch in `minimal.ts` line ~537:
|
||||
```typescript
|
||||
case 'login':
|
||||
await this.handleLoginCommand(command.provider, command.mode);
|
||||
break;
|
||||
```
|
||||
|
||||
Update the method signature:
|
||||
```typescript
|
||||
private async handleLoginCommand(
|
||||
provider?: string,
|
||||
mode?: 'api_key' | 'oauth' | 'auto',
|
||||
): Promise<void> {
|
||||
```
|
||||
|
||||
At the very top of `handleLoginCommand`, before the existing `target` resolution, add the mode-switch fast path:
|
||||
|
||||
```typescript
|
||||
if (mode !== undefined) {
|
||||
const resolvedProvider = provider ?? 'anthropic';
|
||||
if (!AUTH_MODE_PROVIDERS.has(resolvedProvider)) {
|
||||
console.log(
|
||||
`${colors.gray}auth_mode has no effect for ${resolvedProvider}. ` +
|
||||
`It is only supported for: ${[...AUTH_MODE_PROVIDERS].join(', ')}.${colors.reset}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!this.config.currentConfig || !this.config.configPath) {
|
||||
console.log(`${colors.gray}Config not available — cannot persist auth_mode.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, resolvedProvider, mode);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(
|
||||
`${colors.gray}auth_mode for ${resolvedProvider} set to ${colors.reset}${mode}` +
|
||||
`${colors.gray}. Restart Flynn for the change to take effect.${colors.reset}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 6: Add post-credential auth_mode prompt for supported providers**
|
||||
|
||||
In the `anthropic` branch of `handleLoginCommand`, after the credential is stored and before `return`, add (for both the api_key and token paths):
|
||||
|
||||
```typescript
|
||||
// Offer to set auth_mode if config is available and provider supports it
|
||||
if (this.config.currentConfig && this.config.configPath) {
|
||||
const modeInput = (await this.prompt(
|
||||
`${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, 'anthropic', modeInput);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Apply the same block to the `openai` branch (using `'openai'` as the provider string).
|
||||
|
||||
Do **not** add this block to `github` or `zai` branches (they're not in `AUTH_MODE_PROVIDERS`).
|
||||
|
||||
**Step 7: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
**Step 8: Run full test suite**
|
||||
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
**Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/minimal.ts src/frontends/tui/minimal.test.ts
|
||||
git commit -m "feat(tui): implement /login <provider> mode <value> auth mode switching"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Completions for the mode subcommand
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/commands.ts` (getCommandCompletions + getCommandTooltip)
|
||||
- Test: `src/frontends/tui/commands.test.ts`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add to `commands.test.ts` inside `describe('getCommandCompletions', ...)`:
|
||||
|
||||
```typescript
|
||||
it('completes /login <provider> mode values', () => {
|
||||
const completions = getCommandCompletions('/login anthropic mode ');
|
||||
expect(completions).toContain('/login anthropic mode api_key');
|
||||
expect(completions).toContain('/login anthropic mode oauth');
|
||||
expect(completions).toContain('/login anthropic mode auto');
|
||||
});
|
||||
|
||||
it('filters mode completions by partial input', () => {
|
||||
const completions = getCommandCompletions('/login anthropic mode o');
|
||||
expect(completions).toEqual(['/login anthropic mode oauth']);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/commands.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Implement completions**
|
||||
|
||||
In `getCommandCompletions`, add before the generic slash-command fallback:
|
||||
|
||||
```typescript
|
||||
// Complete /login <provider> mode <value>
|
||||
if (trimmed.startsWith('/login ')) {
|
||||
const rest = trimmed.slice('/login '.length);
|
||||
const parts = rest.split(/\s+/);
|
||||
if (parts.length === 3 && parts[1] === 'mode') {
|
||||
const partial = parts[2].toLowerCase();
|
||||
const modes = ['api_key', 'oauth', 'auto'];
|
||||
return modes
|
||||
.filter(m => m.startsWith(partial))
|
||||
.map(m => `/login ${parts[0]} mode ${m}`);
|
||||
}
|
||||
if (parts.length === 2 && parts[1] === 'mod') {
|
||||
return [`/login ${parts[0]} mode`];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/frontends/tui/commands.test.ts
|
||||
```
|
||||
|
||||
Expected: all PASS.
|
||||
|
||||
**Step 5: Run full suite and typecheck**
|
||||
|
||||
```bash
|
||||
pnpm test:run && pnpm typecheck
|
||||
```
|
||||
|
||||
Expected: all pass, no type errors.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/commands.ts src/frontends/tui/commands.test.ts
|
||||
git commit -m "feat(tui): add tab completions for /login mode subcommand"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
pnpm test:run # full suite passes
|
||||
pnpm typecheck # no type errors
|
||||
pnpm lint # no lint errors
|
||||
```
|
||||
|
||||
Manual smoke test:
|
||||
1. `pnpm tui` → type `/login anthropic mode oauth` → confirm config written + restart message
|
||||
2. `pnpm tui` → type `/login zhipuai mode oauth` → confirm warning printed, no config write
|
||||
3. `pnpm tui` → type `/login anthropic` → confirm auth_mode prompt appears after credential entry
|
||||
Reference in New Issue
Block a user