From 72aad2c77cfaad53b413c7f779b5f54eb658b65e Mon Sep 17 00:00:00 2001 From: kite Date: Sun, 14 Jun 2026 10:41:35 +0800 Subject: [PATCH] feat: add interactive provider setup with TUI and documentation --- README.ja-JP.md | 44 +- README.ko-KR.md | 44 +- README.md | 44 +- README.zh-CN.md | 44 +- cmd/opencodereview/config_cmd.go | 166 ++- cmd/opencodereview/config_cmd_test.go | 123 ++ cmd/opencodereview/flags.go | 25 +- cmd/opencodereview/llm_cmd.go | 27 +- cmd/opencodereview/main.go | 3 + cmd/opencodereview/provider_cmd.go | 272 ++++ cmd/opencodereview/provider_tui.go | 1337 ++++++++++++++++++++ cmd/opencodereview/provider_tui_test.go | 514 ++++++++ go.mod | 19 +- go.sum | 44 +- imgs/providers.jpg | Bin 0 -> 97846 bytes internal/llm/providers.go | 116 ++ internal/llm/providers_test.go | 97 ++ internal/llm/resolver.go | 130 +- internal/llm/resolver_test.go | 366 ++++++ pages/src/components/QuickStartSection.tsx | 5 +- pages/src/i18n/en.ts | 13 +- pages/src/i18n/zh.ts | 13 +- pages/src/pages/DocsPage.tsx | 37 +- 23 files changed, 3407 insertions(+), 76 deletions(-) create mode 100644 cmd/opencodereview/provider_cmd.go create mode 100644 cmd/opencodereview/provider_tui.go create mode 100644 cmd/opencodereview/provider_tui_test.go create mode 100644 imgs/providers.jpg create mode 100644 internal/llm/providers.go create mode 100644 internal/llm/providers_test.go diff --git a/README.ja-JP.md b/README.ja-JP.md index 159a1ff..732116c 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -113,18 +113,22 @@ sudo cp dist/opencodereview /usr/local/bin/ocr **コードレビューの前に必ずLLMを設定する必要があります。** +**オプションA: 対話的セットアップ(推奨)** + +```bash +ocr config provider # ビルトインプロバイダーを選択またはカスタムプロバイダーを追加 +ocr config model # アクティブなプロバイダーのモデルを選択 +``` + +![Provider setup](imgs/providers.jpg) + +**オプションB: 手動設定** + ```bash -# オプションA: 対話的な設定 ocr config set llm.url https://api.anthropic.com/v1/messages ocr config set llm.auth_token your-api-key-here ocr config set llm.model claude-opus-4-6 ocr config set llm.use_anthropic true - -# オプションB: 環境変数(最優先) -export OCR_LLM_URL=https://api.anthropic.com/v1/messages -export OCR_LLM_TOKEN=your-api-key-here -export OCR_LLM_MODEL=claude-opus-4-6 -export OCR_USE_ANTHROPIC=true ``` 設定は`~/.opencodereview/config.json`に保存されます。 @@ -133,12 +137,19 @@ export OCR_USE_ANTHROPIC=true ```bash ocr config set llm.auth_header x-api-key -# または -export OCR_LLM_AUTH_HEADER=x-api-key ``` サポートされる値:`x-api-key`、`authorization`(エイリアス:`bearer`)。それ以外の値はエラーになります。 +**オプションC: 環境変数(最優先)** + +```bash +export OCR_LLM_URL=https://api.anthropic.com/v1/messages +export OCR_LLM_TOKEN=your-api-key-here +export OCR_LLM_MODEL=claude-opus-4-6 +export OCR_USE_ANTHROPIC=true +``` + また、Claude Codeの環境変数(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_MODEL`)とも互換性があり、`~/.zshrc` / `~/.bashrc`からこれらのexportをパースします。 > **CC-Switchユーザー向けの注意**: [CC-Switch](https://github.com/farion1231/cc-switch)を[ルーティングサービス](https://www.ccswitch.io/en/docs?section=proxy&item=service)有効で使用している場合、追加設定なしで`llm.url`をCC-Switchのプロキシアドレスに向けることができます: @@ -278,8 +289,11 @@ ocr review \ |---------|-------|-------------| | `ocr review` | `ocr r` | コードレビューを開始 | | `ocr rules check ` | — | ファイルパスに適用されるレビュールールをプレビュー | +| `ocr config provider` | — | 対話的プロバイダーセットアップ(ビルトイン、カスタム、手動) | +| `ocr config model` | — | アクティブなプロバイダーの対話的モデル選択 | | `ocr config set ` | — | 設定値をセット | | `ocr llm test` | — | LLMの疎通テスト | +| `ocr llm providers` | — | ビルトインLLMプロバイダーを一覧表示 | | `ocr viewer` | `ocr v` | `localhost:5483`でWebUIセッションビューアーを起動 | | `ocr version` | — | バージョン情報を表示 | @@ -305,6 +319,11 @@ ocr review \ ## 例 ```bash +# 対話的プロバイダーとモデルのセットアップ +ocr config provider +ocr config model +ocr llm providers + # レビュー対象ファイルをプレビュー(LLM呼び出しなし) ocr review --preview ocr review -c abc123 -p @@ -425,6 +444,13 @@ OCRは4層の優先度チェーンを使ってレビュールールを解決し | キー | 型 | 例 | |-----|------|---------| +| `provider` | string | `anthropic` \| `openai` \| `dashscope` \| `deepseek` \| `z-ai` | +| `providers..api_key` | string | プロバイダー固有のAPIキー | +| `providers..url` | string | プロバイダーのベースURLオーバーライド | +| `providers..protocol` | string | `anthropic` \| `openai` | +| `providers..model` | string | プロバイダーのモデル名 | +| `providers..auth_header` | string | `x-api-key` \| `authorization` | +| `custom_providers..*` | — | `providers..*`と同じフィールド | | `llm.url` | string | `https://api.openai.com/v1/chat/completions` | | `llm.auth_token` | string | `sk-xxxxxxx` | | `llm.auth_header` | string | Anthropicのみ:`x-api-key` \| `authorization` | diff --git a/README.ko-KR.md b/README.ko-KR.md index ef85b01..da712a1 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -113,18 +113,22 @@ sudo cp dist/opencodereview /usr/local/bin/ocr **코드 리뷰를 실행하기 전에 반드시 LLM을 설정해야 합니다.** +**Option A: 대화형 설정 (권장)** + +```bash +ocr config provider # built-in provider 선택 또는 custom provider 추가 +ocr config model # 활성 provider의 model 선택 +``` + +![Provider setup](imgs/providers.jpg) + +**Option B: 수동 설정** + ```bash -# Option A: 대화형 config ocr config set llm.url https://api.anthropic.com/v1/messages ocr config set llm.auth_token your-api-key-here ocr config set llm.model claude-opus-4-6 ocr config set llm.use_anthropic true - -# Option B: 환경 변수(가장 높은 우선순위) -export OCR_LLM_URL=https://api.anthropic.com/v1/messages -export OCR_LLM_TOKEN=your-api-key-here -export OCR_LLM_MODEL=claude-opus-4-6 -export OCR_USE_ANTHROPIC=true ``` config는 `~/.opencodereview/config.json`에 저장됩니다. @@ -133,12 +137,19 @@ config는 `~/.opencodereview/config.json`에 저장됩니다. ```bash ocr config set llm.auth_header x-api-key -# 또는 -export OCR_LLM_AUTH_HEADER=x-api-key ``` 지원되는 값: `x-api-key`, `authorization` (별칭: `bearer`). 그 외 값은 오류로 처리됩니다. +**Option C: 환경 변수(가장 높은 우선순위)** + +```bash +export OCR_LLM_URL=https://api.anthropic.com/v1/messages +export OCR_LLM_TOKEN=your-api-key-here +export OCR_LLM_MODEL=claude-opus-4-6 +export OCR_USE_ANTHROPIC=true +``` + Claude Code 환경 변수(`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`)와도 호환되며, `~/.zshrc` / `~/.bashrc`의 export도 파싱합니다. > **CC-Switch 사용자 참고**: [CC-Switch](https://github.com/farion1231/cc-switch)를 [routing service](https://www.ccswitch.io/en/docs?section=proxy&item=service)와 함께 사용한다면, 추가 설정 없이 `llm.url`을 CC-Switch proxy 주소로 지정할 수 있습니다. @@ -278,8 +289,11 @@ ocr review \ |---------|-------|-------------| | `ocr review` | `ocr r` | 코드 리뷰 시작 | | `ocr rules check ` | - | 파일 경로에 적용될 리뷰 rule 미리보기 | +| `ocr config provider` | - | 대화형 provider 설정 (built-in, custom, 수동) | +| `ocr config model` | - | 활성 provider의 대화형 model 선택 | | `ocr config set ` | - | config 값 설정 | | `ocr llm test` | - | LLM 연결 테스트 | +| `ocr llm providers` | - | built-in LLM provider 목록 표시 | | `ocr viewer` | `ocr v` | `localhost:5483`에서 WebUI session viewer 실행 | | `ocr version` | - | version 정보 표시 | @@ -305,6 +319,11 @@ ocr review \ ## Examples ```bash +# 대화형 provider 및 model 설정 +ocr config provider +ocr config model +ocr llm providers + # 리뷰 대상 파일 미리보기(LLM call 없음) ocr review --preview ocr review -c abc123 -p @@ -383,6 +402,13 @@ Config file: `~/.opencodereview/config.json` | Key | Type | Example | |-----|------|---------| +| `provider` | string | `anthropic` \| `openai` \| `dashscope` \| `deepseek` \| `z-ai` | +| `providers..api_key` | string | Provider별 API key | +| `providers..url` | string | Provider base URL override | +| `providers..protocol` | string | `anthropic` \| `openai` | +| `providers..model` | string | Provider의 model 이름 | +| `providers..auth_header` | string | `x-api-key` \| `authorization` | +| `custom_providers..*` | — | `providers..*`과 동일한 필드 | | `llm.url` | string | `https://api.openai.com/v1/chat/completions` | | `llm.auth_token` | string | `sk-xxxxxxx` | | `llm.auth_header` | string | Anthropic only: `x-api-key` \| `authorization` | diff --git a/README.md b/README.md index 56af8e8..f639c27 100644 --- a/README.md +++ b/README.md @@ -113,18 +113,22 @@ sudo cp dist/opencodereview /usr/local/bin/ocr **You must configure an LLM before reviewing code.** +**Option A: Interactive setup (Recommended)** + +```bash +ocr config provider # Select a built-in provider or add a custom one +ocr config model # Pick a model for the active provider +``` + +![Provider setup](imgs/providers.jpg) + +**Option B: Manual config** + ```bash -# Option A: Interactive config ocr config set llm.url https://api.anthropic.com/v1/messages ocr config set llm.auth_token your-api-key-here ocr config set llm.model claude-opus-4-6 ocr config set llm.use_anthropic true - -# Option B: Environment variables (highest priority) -export OCR_LLM_URL=https://api.anthropic.com/v1/messages -export OCR_LLM_TOKEN=your-api-key-here -export OCR_LLM_MODEL=claude-opus-4-6 -export OCR_USE_ANTHROPIC=true ``` Config is stored in `~/.opencodereview/config.json`. @@ -133,12 +137,19 @@ Config is stored in `~/.opencodereview/config.json`. ```bash ocr config set llm.auth_header x-api-key -# or -export OCR_LLM_AUTH_HEADER=x-api-key ``` Supported values: `x-api-key`, `authorization` (alias: `bearer`). Other values are rejected with an error. +**Option C: Environment variables (highest priority)** + +```bash +export OCR_LLM_URL=https://api.anthropic.com/v1/messages +export OCR_LLM_TOKEN=your-api-key-here +export OCR_LLM_MODEL=claude-opus-4-6 +export OCR_USE_ANTHROPIC=true +``` + It is also compatible with Claude Code environment variables (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) and parses `~/.zshrc` / `~/.bashrc` for those exports. > **Note for CC-Switch Users**: If you are using [CC-Switch](https://github.com/farion1231/cc-switch) with [routing service](https://www.ccswitch.io/en/docs?section=proxy&item=service) enabled, you can point `llm.url` to the CC-Switch proxy address without additional configuration: @@ -280,8 +291,11 @@ See the [`examples/`](./examples/) directory for integration examples: |---------|-------|-------------| | `ocr review` | `ocr r` | Start a code review | | `ocr rules check ` | — | Preview which review rule applies to a file path | +| `ocr config provider` | — | Interactive provider setup (built-in, custom, or manual) | +| `ocr config model` | — | Interactive model selection for the active provider | | `ocr config set ` | — | Set configuration values | | `ocr llm test` | — | Test LLM connectivity | +| `ocr llm providers` | — | List built-in LLM providers | | `ocr viewer` | `ocr v` | Launch WebUI session viewer on `localhost:5483` | | `ocr version` | — | Show version info | @@ -307,6 +321,11 @@ See the [`examples/`](./examples/) directory for integration examples: ## Examples ```bash +# Interactive provider and model setup +ocr config provider +ocr config model +ocr llm providers + # Preview which files will be reviewed (no LLM calls) ocr review --preview ocr review -c abc123 -p @@ -427,6 +446,13 @@ Config file: `~/.opencodereview/config.json` | Key | Type | Example | |-----|------|---------| +| `provider` | string | `anthropic` \| `openai` \| `dashscope` \| `deepseek` \| `z-ai` | +| `providers..api_key` | string | Provider-specific API key | +| `providers..url` | string | Provider base URL override | +| `providers..protocol` | string | `anthropic` \| `openai` | +| `providers..model` | string | Model name for the provider | +| `providers..auth_header` | string | `x-api-key` \| `authorization` | +| `custom_providers..*` | — | Same fields as `providers..*` | | `llm.url` | string | `https://api.openai.com/v1/chat/completions` | | `llm.auth_token` | string | `sk-xxxxxxx` | | `llm.auth_header` | string | Anthropic only: `x-api-key` \| `authorization` | diff --git a/README.zh-CN.md b/README.zh-CN.md index d82715a..ec41f69 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -113,18 +113,22 @@ sudo cp dist/opencodereview /usr/local/bin/ocr **在审查代码之前,必须先配置 LLM。** +**方式 A:交互式设置(推荐)** + +```bash +ocr config provider # 选择内置供应商或添加自定义供应商 +ocr config model # 为当前供应商选择模型 +``` + +![Provider setup](imgs/providers.jpg) + +**方式 B:手动配置** + ```bash -# 方式 A:交互式配置 ocr config set llm.url https://api.anthropic.com/v1/messages ocr config set llm.auth_token your-api-key-here ocr config set llm.model claude-opus-4-6 ocr config set llm.use_anthropic true - -# 方式 B:环境变量(优先级最高) -export OCR_LLM_URL=https://api.anthropic.com/v1/messages -export OCR_LLM_TOKEN=your-api-key-here -export OCR_LLM_MODEL=claude-opus-4-6 -export OCR_USE_ANTHROPIC=true ``` 配置存储于 `~/.opencodereview/config.json`。 @@ -133,12 +137,19 @@ export OCR_USE_ANTHROPIC=true ```bash ocr config set llm.auth_header x-api-key -# 或 -export OCR_LLM_AUTH_HEADER=x-api-key ``` 支持的值:`x-api-key`、`authorization`(别名:`bearer`)。其他值会直接报错。 +**方式 C:环境变量(优先级最高)** + +```bash +export OCR_LLM_URL=https://api.anthropic.com/v1/messages +export OCR_LLM_TOKEN=your-api-key-here +export OCR_LLM_MODEL=claude-opus-4-6 +export OCR_USE_ANTHROPIC=true +``` + 同时兼容了 Claude Code 环境变量(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_MODEL`),并解析 `~/.zshrc` / `~/.bashrc` 中的相关导出。 > **CC-Switch 用户特别提醒**:如果你使用 [CC-Switch](https://github.com/farion1231/cc-switch) 并开启了[路由服务](https://www.ccswitch.io/zh/docs?section=proxy&item=service),可以将 `llm.url` 配置成 CC-Switch 启动的代理地址,无需额外配置: @@ -278,8 +289,11 @@ ocr review \ |------|------|------| | `ocr review` | `ocr r` | 开始代码审查 | | `ocr rules check ` | — | 预览某个文件路径生效的审查规则 | +| `ocr config provider` | — | 交互式供应商设置(内置、自定义或手动) | +| `ocr config model` | — | 为当前供应商交互式选择模型 | | `ocr config set ` | — | 设置配置项 | | `ocr llm test` | — | 测试 LLM 连通性 | +| `ocr llm providers` | — | 列出内置 LLM 供应商 | | `ocr viewer` | `ocr v` | 启动 WebUI 会话查看器,地址 `localhost:5483` | | `ocr version` | — | 显示版本信息 | @@ -305,6 +319,11 @@ ocr review \ ## 示例 ```bash +# 交互式供应商和模型设置 +ocr config provider +ocr config model +ocr llm providers + # 预览将被审查的文件(不调用 LLM) ocr review --preview ocr review -c abc123 -p @@ -415,6 +434,13 @@ OCR 通过四层优先级链解析评审规则。每层采用首次匹配原则 | 键 | 类型 | 示例 | |----|------|------| +| `provider` | string | `anthropic` \| `openai` \| `dashscope` \| `deepseek` \| `z-ai` | +| `providers..api_key` | string | 供应商 API 密钥 | +| `providers..url` | string | 供应商 Base URL 覆盖 | +| `providers..protocol` | string | `anthropic` \| `openai` | +| `providers..model` | string | 供应商模型名称 | +| `providers..auth_header` | string | `x-api-key` \| `authorization` | +| `custom_providers..*` | — | 与 `providers..*` 相同的字段 | | `llm.url` | string | `https://api.openai.com/v1/chat/completions` | | `llm.auth_token` | string | `sk-xxxxxxx` | | `llm.auth_header` | string | 仅 Anthropic:`x-api-key` \| `authorization` | diff --git a/cmd/opencodereview/config_cmd.go b/cmd/opencodereview/config_cmd.go index 6cf1aca..50b16ee 100644 --- a/cmd/opencodereview/config_cmd.go +++ b/cmd/opencodereview/config_cmd.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/open-code-review/open-code-review/internal/llm" ) @@ -25,6 +26,19 @@ func runConfig(args []string) error { return nil } + switch args[0] { + case "provider": + if len(args) != 1 { + return fmt.Errorf("config provider does not accept arguments; use 'ocr config set provider ' for non-interactive setup") + } + return runConfigProvider() + case "model": + if len(args) != 1 { + return fmt.Errorf("config model does not accept arguments; use 'ocr config set model ' for non-interactive setup") + } + return runConfigModel() + } + action, err := parseConfigArgs(args) if err != nil { return err @@ -53,29 +67,38 @@ func runConfigSet(key, value string) error { return err } - dir := filepath.Dir(configPath) - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("create config dir: %w", err) + if err := saveConfig(configPath, cfg); err != nil { + return err } - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return fmt.Errorf("marshal config: %w", err) + displayValue := value + normalizedKey := strings.ToLower(strings.ReplaceAll(key, "_", "")) + if strings.HasSuffix(normalizedKey, "apikey") || strings.HasSuffix(normalizedKey, "authtoken") { + displayValue = maskKey(value) } - - if err := os.WriteFile(configPath, data, 0o644); err != nil { - return fmt.Errorf("write config: %w", err) - } - - fmt.Printf("Set %s = %s\n", key, value) + fmt.Printf("Set %s = %s\n", key, displayValue) return nil } +// ProviderEntry holds per-provider configuration in the providers map. +type ProviderEntry struct { + APIKey string `json:"api_key,omitempty"` + URL string `json:"url,omitempty"` + Protocol string `json:"protocol,omitempty"` + Model string `json:"model,omitempty"` + AuthHeader string `json:"auth_header,omitempty"` + ExtraBody map[string]any `json:"extra_body,omitempty"` +} + // Config represents the user-level configuration file (~/.opencodereview/config.json). type Config struct { - Llm LlmConfig `json:"llm,omitempty"` - Language string `json:"language,omitempty"` // Output language, defaults to Chinese when empty - Telemetry *TelemetryConfig `json:"telemetry,omitempty"` // Telemetry/observability settings + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` + Providers map[string]ProviderEntry `json:"providers,omitempty"` + CustomProviders map[string]ProviderEntry `json:"custom_providers,omitempty"` + Llm LlmConfig `json:"llm,omitempty"` + Language string `json:"language,omitempty"` + Telemetry *TelemetryConfig `json:"telemetry,omitempty"` } type LlmConfig struct { @@ -127,7 +150,55 @@ func LoadAppConfig(path string) (*Config, error) { } func setConfigValue(cfg *Config, key, value string) error { + // Handle providers.. paths. + if strings.HasPrefix(key, "providers.") { + return setProviderValue(cfg, key, value) + } + if strings.HasPrefix(key, "custom_providers.") { + return setCustomProviderValue(cfg, key, value) + } + switch key { + case "provider": + if cfg.Provider != value { + cfg.Model = "" + } + cfg.Provider = value + if _, isPreset := llm.LookupProvider(value); isPreset { + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderEntry) + } + if _, exists := cfg.Providers[value]; !exists { + cfg.Providers[value] = ProviderEntry{} + } + } else { + if cfg.CustomProviders == nil { + cfg.CustomProviders = make(map[string]ProviderEntry) + } + if _, exists := cfg.CustomProviders[value]; !exists { + cfg.CustomProviders[value] = ProviderEntry{} + } + } + case "model": + if cfg.Provider != "" { + if _, isPreset := llm.LookupProvider(cfg.Provider); isPreset { + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderEntry) + } + entry := cfg.Providers[cfg.Provider] + entry.Model = value + cfg.Providers[cfg.Provider] = entry + } else { + if cfg.CustomProviders == nil { + cfg.CustomProviders = make(map[string]ProviderEntry) + } + entry := cfg.CustomProviders[cfg.Provider] + entry.Model = value + cfg.CustomProviders[cfg.Provider] = entry + } + } else { + cfg.Model = value + } case "llm.url", "llm.URL": cfg.Llm.URL = value case "llm.auth_token", "llm.AuthToken": @@ -175,11 +246,74 @@ func setConfigValue(cfg *Config, key, value string) error { } cfg.Llm.ExtraBody = m default: - return fmt.Errorf("unknown config key: %s\nSupported keys: llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging", key) + return fmt.Errorf("unknown config key: %s\nSupported keys: provider, model, providers.., custom_providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging", key) } return nil } +func applyProviderField(entry *ProviderEntry, field, key, value string) error { + switch field { + case "api_key": + entry.APIKey = value + case "url": + entry.URL = value + case "protocol": + if value != "anthropic" && value != "openai" { + return fmt.Errorf("invalid protocol %q: must be \"anthropic\" or \"openai\"", value) + } + entry.Protocol = value + case "model": + entry.Model = value + case "auth_header": + normalized, err := llm.NormalizeAuthHeader(value) + if err != nil { + return err + } + entry.AuthHeader = normalized + case "extra_body": + var m map[string]any + if err := json.Unmarshal([]byte(value), &m); err != nil { + return fmt.Errorf("invalid JSON for %s: %w", key, err) + } + entry.ExtraBody = m + default: + return fmt.Errorf("unknown provider field %q: supported fields are api_key, url, protocol, model, auth_header, extra_body", field) + } + return nil +} + +func setProviderValue(cfg *Config, key, value string) error { + parts := strings.SplitN(key, ".", 3) + if len(parts) != 3 || parts[1] == "" || parts[2] == "" { + return fmt.Errorf("invalid provider key %q: expected providers..", key) + } + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderEntry) + } + entry := cfg.Providers[parts[1]] + if err := applyProviderField(&entry, parts[2], key, value); err != nil { + return err + } + cfg.Providers[parts[1]] = entry + return nil +} + +func setCustomProviderValue(cfg *Config, key, value string) error { + parts := strings.SplitN(key, ".", 3) + if len(parts) != 3 || parts[1] == "" || parts[2] == "" { + return fmt.Errorf("invalid custom provider key %q: expected custom_providers..", key) + } + if cfg.CustomProviders == nil { + cfg.CustomProviders = make(map[string]ProviderEntry) + } + entry := cfg.CustomProviders[parts[1]] + if err := applyProviderField(&entry, parts[2], key, value); err != nil { + return err + } + cfg.CustomProviders[parts[1]] = entry + return nil +} + func (c *Config) ensureTelemetry() { if c.Telemetry == nil { c.Telemetry = &TelemetryConfig{} diff --git a/cmd/opencodereview/config_cmd_test.go b/cmd/opencodereview/config_cmd_test.go index cb31424..34b4234 100644 --- a/cmd/opencodereview/config_cmd_test.go +++ b/cmd/opencodereview/config_cmd_test.go @@ -21,3 +21,126 @@ func TestSetConfigValueAuthHeaderRejectsCustomHeader(t *testing.T) { t.Fatal("expected error for unsupported auth_header, got nil") } } + +func TestSetConfigValueProvider(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "provider", "anthropic"); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.Provider != "anthropic" { + t.Errorf("Provider = %q, want %q", cfg.Provider, "anthropic") + } +} + +func TestSetConfigValueModel(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "model", "claude-opus-4-6"); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.Model != "claude-opus-4-6" { + t.Errorf("Model = %q, want %q", cfg.Model, "claude-opus-4-6") + } +} + +func TestSetConfigValueModelWithProvider(t *testing.T) { + cfg := &Config{ + Provider: "anthropic", + Providers: map[string]ProviderEntry{ + "anthropic": {APIKey: "sk-test"}, + }, + } + + if err := setConfigValue(cfg, "model", "claude-opus-4-6"); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.Providers["anthropic"].Model != "claude-opus-4-6" { + t.Errorf("entry Model = %q, want %q", cfg.Providers["anthropic"].Model, "claude-opus-4-6") + } + if cfg.Model != "" { + t.Errorf("top-level Model = %q, want empty (should write to provider entry)", cfg.Model) + } +} + +func TestSetConfigValueProviderEntry(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "providers.anthropic.api_key", "sk-ant-test"); err != nil { + t.Fatalf("setConfigValue api_key: %v", err) + } + if cfg.Providers["anthropic"].APIKey != "sk-ant-test" { + t.Errorf("api_key = %q, want %q", cfg.Providers["anthropic"].APIKey, "sk-ant-test") + } + + if err := setConfigValue(cfg, "providers.anthropic.model", "claude-opus-4-6"); err != nil { + t.Fatalf("setConfigValue model: %v", err) + } + if cfg.Providers["anthropic"].Model != "claude-opus-4-6" { + t.Errorf("model = %q, want %q", cfg.Providers["anthropic"].Model, "claude-opus-4-6") + } +} + +func TestSetConfigValueProviderEntryProtocol(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "custom_providers.custom.protocol", "openai"); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.CustomProviders["custom"].Protocol != "openai" { + t.Errorf("protocol = %q, want %q", cfg.CustomProviders["custom"].Protocol, "openai") + } + + if err := setConfigValue(cfg, "custom_providers.custom.protocol", "invalid"); err == nil { + t.Fatal("expected error for invalid protocol") + } +} + +func TestSetConfigValueProviderEntryInvalidKey(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "providers.anthropic.unknown_field", "value"); err == nil { + t.Fatal("expected error for unknown provider field") + } +} + +func TestSetConfigValueProviderEntryInvalidPath(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "providers.anthropic", "value"); err == nil { + t.Fatal("expected error for incomplete provider path") + } +} + +func TestSetConfigValueProviderEntryExtraBody(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "providers.anthropic.extra_body", `{"thinking":{"type":"disabled"}}`); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.Providers["anthropic"].ExtraBody == nil { + t.Fatal("extra_body should not be nil") + } + if _, ok := cfg.Providers["anthropic"].ExtraBody["thinking"]; !ok { + t.Error("extra_body missing 'thinking' key") + } +} + +func TestSetConfigValueModelWithCustomProvider(t *testing.T) { + cfg := &Config{ + Provider: "my-gateway", + CustomProviders: map[string]ProviderEntry{ + "my-gateway": {URL: "https://gw.example.com/v1", Protocol: "openai"}, + }, + } + + if err := setConfigValue(cfg, "model", "llama-3-70b"); err != nil { + t.Fatalf("setConfigValue: %v", err) + } + if cfg.CustomProviders["my-gateway"].Model != "llama-3-70b" { + t.Errorf("entry Model = %q, want %q", cfg.CustomProviders["my-gateway"].Model, "llama-3-70b") + } + if cfg.Model != "" { + t.Errorf("top-level Model = %q, want empty (should write to custom provider entry)", cfg.Model) + } +} diff --git a/cmd/opencodereview/flags.go b/cmd/opencodereview/flags.go index 3b62b5f..3e13bb5 100644 --- a/cmd/opencodereview/flags.go +++ b/cmd/opencodereview/flags.go @@ -249,7 +249,7 @@ func parseConfigArgs(args []string) (configAction, error) { value: args[2], }, nil default: - return configAction{}, fmt.Errorf("unknown config sub-command: %s\nAvailable: set", subCmd) + return configAction{}, fmt.Errorf("unknown config sub-command: %s\nAvailable: set, provider, model", subCmd) } } @@ -258,8 +258,29 @@ func printConfigUsage() { Usage: ocr config set + ocr config provider Interactive provider setup + ocr config model Interactive model selection Examples: + # Provider setup (interactive) + ocr config provider + ocr config model + + # Provider setup (non-interactive) + ocr config set provider anthropic + ocr config set model claude-opus-4-6 + # Set API key via environment variable (recommended) or config: + # export ANTHROPIC_API_KEY=sk-ant-xxx + ocr config set providers.anthropic.api_key "$ANTHROPIC_API_KEY" + + # Custom provider + ocr config set provider my-gateway + ocr config set providers.my-gateway.url https://gateway.internal.com/v1 + ocr config set providers.my-gateway.protocol openai + ocr config set providers.my-gateway.model llama-3-70b + ocr config set providers.my-gateway.api_key "$MY_API_KEY" + + # Legacy endpoint configuration ocr config set llm.url https://xx/v1/openai/chat/completions ocr config set llm.auth_token xxxxxxxxxx ocr config set llm.auth_header x-api-key @@ -268,5 +289,5 @@ Examples: ocr config set language English ocr config set telemetry.enabled true -Supported keys: llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging`) +Supported keys: provider, model, providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging`) } diff --git a/cmd/opencodereview/llm_cmd.go b/cmd/opencodereview/llm_cmd.go index 84088c9..95cf861 100644 --- a/cmd/opencodereview/llm_cmd.go +++ b/cmd/opencodereview/llm_cmd.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/open-code-review/open-code-review/internal/config/template" "github.com/open-code-review/open-code-review/internal/config/testconnection" "github.com/open-code-review/open-code-review/internal/llm" ) @@ -19,6 +18,9 @@ func runLLM(args []string) error { switch args[0] { case "test": return runLLMTest() + case "providers": + runLLMProviders() + return nil default: return fmt.Errorf("unknown llm sub-command: %s\nRun 'ocr llm' for usage", args[0]) } @@ -53,11 +55,6 @@ func runLLMTest() error { timeout = time.Duration(task.Timeout) * time.Second } - tpl, err := template.LoadDefault() - if err != nil { - return fmt.Errorf("load default template: %w", err) - } - llmClient := llm.NewLLMClient(ep) messages := make([]llm.Message, 0, len(task.Messages)) @@ -71,7 +68,7 @@ func runLLMTest() error { return llmClient.CompletionsWithCtx(ctx, llm.ChatRequest{ Model: ep.Model, Messages: messages, - MaxTokens: tpl.MaxTokens, + MaxTokens: 256, }) }() if err != nil { @@ -89,6 +86,18 @@ func runLLMTest() error { return nil } +func runLLMProviders() { + providers := llm.ListProviders() + fmt.Println("\nBuilt-in providers:") + fmt.Printf(" %-14s %-10s %s\n", "NAME", "PROTOCOL", "BASE URL") + fmt.Printf(" %-14s %-10s %s\n", "----", "--------", "--------") + for _, p := range providers { + fmt.Printf(" %-14s %-10s %s\n", p.Name, p.Protocol, p.BaseURL) + } + fmt.Println("\nUse 'ocr config provider' to configure a provider interactively.") + fmt.Println("Use 'ocr config set provider ' to switch providers non-interactively.") +} + func printLLMUsage() { fmt.Println(`LLM utility commands. @@ -97,7 +106,9 @@ Usage: Sub-commands: test Send a test conversation to the configured LLM model + providers List all built-in LLM providers Examples: - ocr llm test Verify LLM connectivity and configuration`) + ocr llm test Verify LLM connectivity and configuration + ocr llm providers List available built-in providers`) } diff --git a/cmd/opencodereview/main.go b/cmd/opencodereview/main.go index d97d9c9..50c5b2c 100644 --- a/cmd/opencodereview/main.go +++ b/cmd/opencodereview/main.go @@ -79,8 +79,11 @@ Commands: Examples: ocr review --from master --to dev Review diff range ocr review --commit abc123 Review a single commit + ocr config provider Interactive provider setup + ocr config model Interactive model selection ocr config set llm.model opus-4-6 Set a config value ocr llm test Test LLM connectivity + ocr llm providers List built-in providers ocr version Show version info Use "ocr review -h" for more information about review. diff --git a/cmd/opencodereview/provider_cmd.go b/cmd/opencodereview/provider_cmd.go new file mode 100644 index 0000000..e62a6b7 --- /dev/null +++ b/cmd/opencodereview/provider_cmd.go @@ -0,0 +1,272 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + tea "charm.land/bubbletea/v2" + + "github.com/open-code-review/open-code-review/internal/llm" +) + +func runConfigProvider() error { + configPath, err := defaultConfigPath() + if err != nil { + return err + } + + cfg, err := loadOrCreateConfig(configPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + m := newProviderTUI(cfg) + p := tea.NewProgram(m) + finalModel, err := p.Run() + if err != nil { + return fmt.Errorf("TUI error: %w", err) + } + + final := finalModel.(providerTUIModel) + if !final.confirmed { + fmt.Println("Cancelled.") + return nil + } + + result := final.result() + + if result.isManual { + return applyManualConfig(configPath, cfg, result) + } + + if result.isCustom { + return applyCustomProviderConfig(configPath, cfg, result) + } + + return applyOfficialProviderConfig(configPath, cfg, result) +} + +func applyManualConfig(configPath string, cfg *Config, result providerTUIResult) error { + if result.url == "" { + return fmt.Errorf("URL is required for manual configuration") + } + if result.model == "" { + return fmt.Errorf("model is required for manual configuration") + } + + cfg.Provider = "" + cfg.Model = "" + cfg.Llm.URL = result.url + cfg.Llm.Model = result.model + cfg.Llm.AuthToken = result.apiKey + + if err := saveConfig(configPath, cfg); err != nil { + return err + } + + fmt.Println("\nManual configuration saved.") + fmt.Printf("URL: %s\n", result.url) + fmt.Printf("Model: %s\n", result.model) + + fmt.Println("\nTesting connection...") + if err := runLLMTest(); err != nil { + fmt.Fprintf(os.Stderr, "Connection test failed: %v\n", err) + fmt.Fprintln(os.Stderr, "Configuration has been saved. Fix the issue and run 'ocr llm test' to re-verify.") + return nil + } + + return nil +} + +func applyCustomProviderConfig(configPath string, cfg *Config, result providerTUIResult) error { + if result.provider == "" { + return fmt.Errorf("provider name is required") + } + if result.model == "" { + return fmt.Errorf("model is required") + } + + if cfg.CustomProviders == nil { + cfg.CustomProviders = make(map[string]ProviderEntry) + } + + entry := cfg.CustomProviders[result.provider] + entry.Model = result.model + if result.url != "" { + entry.URL = result.url + } + if result.protocol != "" { + entry.Protocol = result.protocol + } + if result.authHeader != "" { + entry.AuthHeader = result.authHeader + } + if result.apiKey != "" { + entry.APIKey = result.apiKey + } + cfg.CustomProviders[result.provider] = entry + + if cfg.Provider != result.provider { + cfg.Model = "" + } + cfg.Provider = result.provider + + if err := saveConfig(configPath, cfg); err != nil { + return err + } + + fmt.Printf("\nProvider set to: %s (custom)\n", result.provider) + fmt.Printf("Model: %s\n", result.model) + + fmt.Println("\nTesting connection...") + if err := runLLMTest(); err != nil { + fmt.Fprintf(os.Stderr, "Connection test failed: %v\n", err) + fmt.Fprintln(os.Stderr, "Provider configuration has been saved. Fix the issue and run 'ocr llm test' to re-verify.") + return nil + } + + fmt.Println("\nTip: run 'ocr config model' to switch model later.") + return nil +} + +func applyOfficialProviderConfig(configPath string, cfg *Config, result providerTUIResult) error { + if result.provider == "" || result.model == "" { + return fmt.Errorf("provider and model are required") + } + + preset, isPreset := llm.LookupProvider(result.provider) + + if result.apiKey == "" { + if isPreset && preset.EnvVar != "" { + if os.Getenv(preset.EnvVar) == "" { + return fmt.Errorf("API key is required for provider %s (configure it or set $%s)", result.provider, preset.EnvVar) + } + } else { + return fmt.Errorf("API key is required for provider %s", result.provider) + } + } + + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderEntry) + } + + entry := cfg.Providers[result.provider] + entry.Model = result.model + if result.apiKey != "" { + entry.APIKey = result.apiKey + } + cfg.Providers[result.provider] = entry + + if cfg.Provider != result.provider { + cfg.Model = "" + } + cfg.Provider = result.provider + + if err := saveConfig(configPath, cfg); err != nil { + return err + } + + fmt.Printf("\nProvider set to: %s\n", result.provider) + fmt.Printf("Model: %s\n", result.model) + + fmt.Println("\nTesting connection...") + if err := runLLMTest(); err != nil { + fmt.Fprintf(os.Stderr, "Connection test failed: %v\n", err) + fmt.Fprintln(os.Stderr, "Provider configuration has been saved. Fix the issue and run 'ocr llm test' to re-verify.") + return nil + } + + fmt.Println("\nTip: run 'ocr config model' to switch model later.") + return nil +} + +func runConfigModel() error { + configPath, err := defaultConfigPath() + if err != nil { + return err + } + + cfg, err := loadOrCreateConfig(configPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + if cfg.Provider == "" { + return fmt.Errorf("no provider configured. Run 'ocr config provider' first") + } + + preset, isPreset := llm.LookupProvider(cfg.Provider) + if !isPreset { + return fmt.Errorf("provider %q is not a preset provider; use 'ocr config set providers.%s.model ' instead", cfg.Provider, cfg.Provider) + } + + currentModel := "" + if entry, ok := cfg.Providers[cfg.Provider]; ok { + currentModel = entry.Model + } + if currentModel == "" { + currentModel = cfg.Model + } + + m := newModelTUI(preset, currentModel) + p := tea.NewProgram(m) + finalModel, err := p.Run() + if err != nil { + return fmt.Errorf("TUI error: %w", err) + } + + final := finalModel.(modelTUIModel) + if final.cancelled { + fmt.Println("Cancelled.") + return nil + } + + selectedModel := final.selectedModel() + if selectedModel == "" { + return fmt.Errorf("model name cannot be empty") + } + + if cfg.Providers == nil { + cfg.Providers = make(map[string]ProviderEntry) + } + entry := cfg.Providers[cfg.Provider] + entry.Model = selectedModel + cfg.Providers[cfg.Provider] = entry + + if err := saveConfig(configPath, cfg); err != nil { + return err + } + + fmt.Printf("\nModel set to: %s\n", selectedModel) + return nil +} + +func saveConfig(path string, cfg *Config) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create config dir: %w", err) + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + if err := os.WriteFile(path, data, 0o600); err != nil { + return fmt.Errorf("write config: %w", err) + } + if err := os.Chmod(path, 0o600); err != nil { + return fmt.Errorf("chmod config: %w", err) + } + return nil +} + +func maskKey(key string) string { + if key == "" { + return "(not set)" + } + if len(key) <= 8 { + return "***" + } + return key[:4] + "***" + key[len(key)-4:] +} diff --git a/cmd/opencodereview/provider_tui.go b/cmd/opencodereview/provider_tui.go new file mode 100644 index 0000000..9bf70d2 --- /dev/null +++ b/cmd/opencodereview/provider_tui.go @@ -0,0 +1,1337 @@ +package main + +import ( + "fmt" + "os" + "sort" + "strings" + + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + + "github.com/open-code-review/open-code-review/internal/llm" +) + +type tuiStep int + +const ( + stepProvider tuiStep = iota + stepModel + stepAPIKey +) + +type providerTab int + +const ( + tabOfficial providerTab = iota + tabCustom + tabManual + tabCount // sentinel — must remain last +) + +type customProviderStep int + +const ( + cpStepName customProviderStep = iota + cpStepProtocol + cpStepBaseURL + cpStepModel + cpStepAPIKey + cpStepAuthHeader +) + +type manualStep int + +const ( + manualStepURL manualStep = iota + manualStepModel + manualStepAuthToken +) + +var cpProtocols = []string{"anthropic", "openai"} + +type customProviderListItem struct { + name string + entry ProviderEntry +} + +type providerTUIResult struct { + provider string + model string + apiKey string + isCustom bool + isManual bool + url string + protocol string + authHeader string +} + +type providerTUIModel struct { + step tuiStep + width int + height int + + activeTab providerTab + + // --- tab: official --- + providers []llm.Provider + officialIdx int + + // --- tab: custom --- + customProviders []customProviderListItem + customIdx int + creatingCustom bool + cpStep customProviderStep + cpProtocolIdx int + cpNameInput textinput.Model + cpURLInput textinput.Model + cpModelInput textinput.Model + cpAuthInput textinput.Model + + // --- tab: manual --- + inManualForm bool + manualStep manualStep + manualURLInput textinput.Model + manualModelInput textinput.Model + manualTokenInput textinput.Model + + // --- shared model/api-key steps (official + existing custom) --- + modelIdx int + customModel bool + modelInput textinput.Model + + apiKeyInput textinput.Model + apiKeyMasked bool + apiKeyOriginal string + + existingCfg *Config + confirmed bool + cancelled bool +} + +func collectCustomProviders(cfg *Config) []customProviderListItem { + if cfg == nil || cfg.CustomProviders == nil { + return nil + } + var out []customProviderListItem + for name, entry := range cfg.CustomProviders { + out = append(out, customProviderListItem{name: name, entry: entry}) + } + sort.Slice(out, func(i, j int) bool { return out[i].name < out[j].name }) + return out +} + +func newProviderTUI(cfg *Config) providerTUIModel { + providers := llm.ListProviders() + + mi := textinput.New() + mi.Placeholder = "model name" + mi.SetWidth(40) + + ai := textinput.New() + ai.Placeholder = "paste your API key here" + ai.SetWidth(50) + ai.EchoMode = textinput.EchoPassword + ai.EchoCharacter = '*' + + cpName := textinput.New() + cpName.Placeholder = "provider name (e.g. my-llm)" + cpName.SetWidth(40) + + cpURL := textinput.New() + cpURL.Placeholder = "enter your API base URL" + cpURL.SetWidth(50) + + cpModel := textinput.New() + cpModel.Placeholder = "model name" + cpModel.SetWidth(40) + + cpAuth := textinput.New() + cpAuth.Placeholder = "optional, leave empty for default (Authorization)" + cpAuth.SetWidth(40) + + manualURL := textinput.New() + manualURL.Placeholder = "enter your API base URL" + manualURL.SetWidth(50) + + manualModel := textinput.New() + manualModel.Placeholder = "enter model name" + manualModel.SetWidth(40) + + manualToken := textinput.New() + manualToken.Placeholder = "enter your auth token" + manualToken.SetWidth(50) + manualToken.EchoMode = textinput.EchoPassword + manualToken.EchoCharacter = '*' + + m := providerTUIModel{ + providers: providers, + existingCfg: cfg, + modelInput: mi, + apiKeyInput: ai, + cpNameInput: cpName, + cpURLInput: cpURL, + cpModelInput: cpModel, + cpAuthInput: cpAuth, + manualURLInput: manualURL, + manualModelInput: manualModel, + manualTokenInput: manualToken, + width: 80, + height: 24, + activeTab: tabOfficial, + customProviders: collectCustomProviders(cfg), + } + + providerFound := false + if cfg.Provider != "" { + for i, p := range providers { + if p.Name == cfg.Provider { + m.officialIdx = i + providerFound = true + break + } + } + + if !providerFound { + m.activeTab = tabCustom + m.customIdx = len(m.customProviders) // default to "Add" option + for i, cp := range m.customProviders { + if cp.name == cfg.Provider { + m.customIdx = i + break + } + } + } + } + + if providerFound { + if entry, ok := cfg.Providers[cfg.Provider]; ok && entry.Model != "" { + selected := providers[m.officialIdx] + found := false + for i, model := range selected.Models { + if model == entry.Model { + m.modelIdx = i + found = true + break + } + } + if !found { + m.modelIdx = len(selected.Models) + m.modelInput.SetValue(entry.Model) + } + } + + if entry, ok := cfg.Providers[cfg.Provider]; ok && entry.APIKey != "" { + m.apiKeyOriginal = entry.APIKey + m.apiKeyMasked = true + } + } + + if cfg.Provider == "" && cfg.Llm.URL != "" { + m.activeTab = tabManual + } + + if cfg.Llm.URL != "" { + m.manualURLInput.SetValue(cfg.Llm.URL) + m.manualModelInput.SetValue(cfg.Llm.Model) + if cfg.Llm.AuthToken != "" { + m.manualTokenInput.SetValue(cfg.Llm.AuthToken) + } + } + + return m +} + +func (m providerTUIModel) Init() tea.Cmd { + return nil +} + +func (m providerTUIModel) currentProvider() llm.Provider { + if m.activeTab != tabOfficial || m.officialIdx >= len(m.providers) { + return llm.Provider{} + } + return m.providers[m.officialIdx] +} + +func (m providerTUIModel) models() []string { + return m.currentProvider().Models +} + +func (m providerTUIModel) isCustomModelItem(idx int) bool { + return idx == len(m.models()) +} + +func (m providerTUIModel) modelCount() int { + return len(m.models()) + 1 +} + +func (m providerTUIModel) customListCount() int { + return len(m.customProviders) + 1 +} + +// --- Update --- + +func (m providerTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + + case tea.KeyPressMsg: + key := msg.String() + + if m.step == stepModel && m.customModel { + return m.updateCustomModelInput(key, msg) + } + + if m.step == stepAPIKey { + return m.updateAPIKeyInput(key, msg) + } + + if m.step == stepProvider && m.creatingCustom { + return m.updateCustomProviderForm(key, msg) + } + + if m.step == stepProvider && m.inManualForm { + return m.updateManualForm(key, msg) + } + + switch key { + case "ctrl+c": + m.cancelled = true + return m, tea.Quit + + case "esc": + if m.step == stepProvider { + m.cancelled = true + return m, tea.Quit + } + m.step-- + return m, nil + + case "enter": + return m.handleEnter() + + case "up", "k": + return m.handleUp() + + case "down", "j": + return m.handleDown() + + case "left", "h": + if m.step == stepProvider { + if m.activeTab > 0 { + m.activeTab-- + } + } + return m, nil + + case "right", "l": + if m.step == stepProvider { + if m.activeTab < tabCount-1 { + m.activeTab++ + } + } + return m, nil + + case "tab": + if m.step == stepProvider { + m.activeTab = (m.activeTab + 1) % tabCount + } + return m, nil + } + + default: + if m.step == stepProvider && m.creatingCustom { + return m.passThroughCPInput(msg) + } + if m.step == stepProvider && m.inManualForm { + return m.passThroughManualInput(msg) + } + if m.step == stepAPIKey { + var cmd tea.Cmd + m.apiKeyInput, cmd = m.apiKeyInput.Update(msg) + return m, cmd + } + if m.step == stepModel && m.customModel { + var cmd tea.Cmd + m.modelInput, cmd = m.modelInput.Update(msg) + return m, cmd + } + } + return m, nil +} + +func (m providerTUIModel) updateCustomModelInput(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch key { + case "esc": + m.customModel = false + m.modelInput.Blur() + m.modelInput.SetValue("") + return m, nil + case "enter": + if m.modelInput.Value() != "" { + m.customModel = false + m.modelInput.Blur() + m.step = stepAPIKey + m.loadExistingAPIKey() + return m, m.apiKeyInput.Focus() + } + return m, nil + default: + var cmd tea.Cmd + m.modelInput, cmd = m.modelInput.Update(msg) + return m, cmd + } +} + +func (m providerTUIModel) updateAPIKeyInput(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch key { + case "esc": + m.apiKeyInput.Blur() + if m.activeTab == tabCustom { + m.step = stepProvider + } else { + m.step = stepModel + } + return m, nil + case "enter": + m.confirmed = true + return m, tea.Quit + case "ctrl+c": + m.cancelled = true + return m, tea.Quit + default: + if m.apiKeyMasked { + if len(key) == 1 { + m.apiKeyMasked = false + m.apiKeyInput.SetValue("") + } else { + return m, nil + } + } + var cmd tea.Cmd + m.apiKeyInput, cmd = m.apiKeyInput.Update(msg) + return m, cmd + } +} + +func (m providerTUIModel) updateCustomProviderForm(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch key { + case "ctrl+c": + m.cancelled = true + return m, tea.Quit + case "esc": + if m.cpStep == cpStepName { + m.creatingCustom = false + m.cpNameInput.Blur() + return m, nil + } + m.blurCPStep() + m.cpStep-- + return m, m.focusCPStep() + case "enter": + return m.handleCustomFormEnter() + case "up", "k": + if m.cpStep == cpStepProtocol && m.cpProtocolIdx > 0 { + m.cpProtocolIdx-- + } + return m, nil + case "down", "j": + if m.cpStep == cpStepProtocol && m.cpProtocolIdx < len(cpProtocols)-1 { + m.cpProtocolIdx++ + } + return m, nil + default: + return m.passThroughCPInput(msg) + } +} + +func (m providerTUIModel) handleCustomFormEnter() (tea.Model, tea.Cmd) { + switch m.cpStep { + case cpStepName: + if m.cpNameInput.Value() == "" { + return m, nil + } + m.cpNameInput.Blur() + m.cpStep = cpStepProtocol + return m, nil + case cpStepProtocol: + m.cpStep = cpStepBaseURL + return m, m.cpURLInput.Focus() + case cpStepBaseURL: + if m.cpURLInput.Value() == "" { + return m, nil + } + m.cpURLInput.Blur() + m.cpStep = cpStepModel + return m, m.cpModelInput.Focus() + case cpStepModel: + if m.cpModelInput.Value() == "" { + return m, nil + } + m.cpModelInput.Blur() + m.cpStep = cpStepAPIKey + m.apiKeyInput.SetValue("") + m.apiKeyMasked = false + return m, m.apiKeyInput.Focus() + case cpStepAPIKey: + m.apiKeyInput.Blur() + m.cpStep = cpStepAuthHeader + return m, m.cpAuthInput.Focus() + case cpStepAuthHeader: + m.cpAuthInput.Blur() + m.confirmed = true + return m, tea.Quit + } + return m, nil +} + +func (m *providerTUIModel) blurCPStep() { + switch m.cpStep { + case cpStepName: + m.cpNameInput.Blur() + case cpStepBaseURL: + m.cpURLInput.Blur() + case cpStepModel: + m.cpModelInput.Blur() + case cpStepAPIKey: + m.apiKeyInput.Blur() + case cpStepAuthHeader: + m.cpAuthInput.Blur() + } +} + +func (m providerTUIModel) focusCPStep() tea.Cmd { + switch m.cpStep { + case cpStepName: + return m.cpNameInput.Focus() + case cpStepBaseURL: + return m.cpURLInput.Focus() + case cpStepModel: + return m.cpModelInput.Focus() + case cpStepAPIKey: + return m.apiKeyInput.Focus() + case cpStepAuthHeader: + return m.cpAuthInput.Focus() + } + return nil +} + +func (m providerTUIModel) passThroughCPInput(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch m.cpStep { + case cpStepName: + m.cpNameInput, cmd = m.cpNameInput.Update(msg) + case cpStepBaseURL: + m.cpURLInput, cmd = m.cpURLInput.Update(msg) + case cpStepModel: + m.cpModelInput, cmd = m.cpModelInput.Update(msg) + case cpStepAPIKey: + if m.apiKeyMasked { + return m, nil + } + m.apiKeyInput, cmd = m.apiKeyInput.Update(msg) + case cpStepAuthHeader: + m.cpAuthInput, cmd = m.cpAuthInput.Update(msg) + } + return m, cmd +} + +func (m providerTUIModel) updateManualForm(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch key { + case "ctrl+c": + m.cancelled = true + return m, tea.Quit + case "esc": + if m.manualStep == manualStepURL { + m.inManualForm = false + m.manualURLInput.Blur() + if m.existingCfg != nil { + m.manualURLInput.SetValue(m.existingCfg.Llm.URL) + m.manualModelInput.SetValue(m.existingCfg.Llm.Model) + m.manualTokenInput.SetValue(m.existingCfg.Llm.AuthToken) + } else { + m.manualURLInput.SetValue("") + m.manualModelInput.SetValue("") + m.manualTokenInput.SetValue("") + } + return m, nil + } + m.blurManualStep() + m.manualStep-- + return m, m.focusManualStep() + case "enter": + return m.handleManualFormEnter() + default: + return m.passThroughManualInput(msg) + } +} + +func (m providerTUIModel) handleManualFormEnter() (tea.Model, tea.Cmd) { + switch m.manualStep { + case manualStepURL: + if m.manualURLInput.Value() == "" { + return m, nil + } + m.manualURLInput.Blur() + m.manualStep = manualStepModel + return m, m.manualModelInput.Focus() + case manualStepModel: + if m.manualModelInput.Value() == "" { + return m, nil + } + m.manualModelInput.Blur() + m.manualStep = manualStepAuthToken + return m, m.manualTokenInput.Focus() + case manualStepAuthToken: + m.manualTokenInput.Blur() + m.confirmed = true + return m, tea.Quit + } + return m, nil +} + +func (m *providerTUIModel) blurManualStep() { + switch m.manualStep { + case manualStepURL: + m.manualURLInput.Blur() + case manualStepModel: + m.manualModelInput.Blur() + case manualStepAuthToken: + m.manualTokenInput.Blur() + } +} + +func (m providerTUIModel) focusManualStep() tea.Cmd { + switch m.manualStep { + case manualStepURL: + return m.manualURLInput.Focus() + case manualStepModel: + return m.manualModelInput.Focus() + case manualStepAuthToken: + return m.manualTokenInput.Focus() + } + return nil +} + +func (m providerTUIModel) passThroughManualInput(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch m.manualStep { + case manualStepURL: + m.manualURLInput, cmd = m.manualURLInput.Update(msg) + case manualStepModel: + m.manualModelInput, cmd = m.manualModelInput.Update(msg) + case manualStepAuthToken: + m.manualTokenInput, cmd = m.manualTokenInput.Update(msg) + } + return m, cmd +} + +func (m providerTUIModel) handleEnter() (tea.Model, tea.Cmd) { + switch m.step { + case stepProvider: + switch m.activeTab { + case tabOfficial: + m.step = stepModel + m.modelIdx = 0 + if m.existingCfg != nil { + if entry, ok := m.existingCfg.Providers[m.currentProvider().Name]; ok && entry.Model != "" { + found := false + for i, model := range m.models() { + if model == entry.Model { + m.modelIdx = i + found = true + break + } + } + if !found { + m.modelIdx = len(m.models()) + m.modelInput.SetValue(entry.Model) + } + } + } + return m, nil + + case tabCustom: + addIdx := len(m.customProviders) + if m.customIdx == addIdx { + m.creatingCustom = true + m.cpStep = cpStepName + m.cpProtocolIdx = 1 // default openai + m.cpNameInput.SetValue("") + m.cpURLInput.SetValue("") + m.cpModelInput.SetValue("") + m.cpAuthInput.SetValue("") + m.apiKeyInput.SetValue("") + m.apiKeyMasked = false + return m, m.cpNameInput.Focus() + } + cp := m.customProviders[m.customIdx] + m.step = stepAPIKey + m.apiKeyMasked = false + m.apiKeyOriginal = "" + m.apiKeyInput.SetValue("") + if cp.entry.APIKey != "" { + m.apiKeyOriginal = cp.entry.APIKey + m.apiKeyMasked = true + m.apiKeyInput.SetValue(strings.Repeat("*", 20)) + } + return m, m.apiKeyInput.Focus() + + case tabManual: + m.inManualForm = true + m.manualStep = manualStepURL + return m, m.manualURLInput.Focus() + } + + case stepModel: + if m.isCustomModelItem(m.modelIdx) { + m.customModel = true + return m, m.modelInput.Focus() + } + m.step = stepAPIKey + m.loadExistingAPIKey() + return m, m.apiKeyInput.Focus() + } + return m, nil +} + +func (m providerTUIModel) handleUp() (tea.Model, tea.Cmd) { + switch m.step { + case stepProvider: + switch m.activeTab { + case tabOfficial: + if m.officialIdx > 0 { + m.officialIdx-- + } + case tabCustom: + if m.customIdx > 0 { + m.customIdx-- + } + } + case stepModel: + if m.modelIdx > 0 { + m.modelIdx-- + } + } + return m, nil +} + +func (m providerTUIModel) handleDown() (tea.Model, tea.Cmd) { + switch m.step { + case stepProvider: + switch m.activeTab { + case tabOfficial: + if m.officialIdx < len(m.providers)-1 { + m.officialIdx++ + } + case tabCustom: + if m.customIdx < m.customListCount()-1 { + m.customIdx++ + } + } + case stepModel: + if m.modelIdx < m.modelCount()-1 { + m.modelIdx++ + } + } + return m, nil +} + +func (m *providerTUIModel) loadExistingAPIKey() { + p := m.currentProvider() + m.apiKeyMasked = false + m.apiKeyOriginal = "" + m.apiKeyInput.SetValue("") + if m.existingCfg != nil { + if entry, ok := m.existingCfg.Providers[p.Name]; ok && entry.APIKey != "" { + m.apiKeyOriginal = entry.APIKey + m.apiKeyMasked = true + m.apiKeyInput.SetValue(strings.Repeat("*", 20)) + } + } +} + +func (m providerTUIModel) result() providerTUIResult { + switch m.activeTab { + case tabOfficial: + p := m.currentProvider() + model := "" + if m.modelInput.Value() != "" && (m.customModel || m.isCustomModelItem(m.modelIdx)) { + model = m.modelInput.Value() + } else if m.modelIdx < len(m.models()) { + model = m.models()[m.modelIdx] + } + + apiKey := "" + if m.apiKeyMasked { + apiKey = m.apiKeyOriginal + } else { + apiKey = m.apiKeyInput.Value() + } + + return providerTUIResult{ + provider: p.Name, + model: model, + apiKey: apiKey, + } + + case tabCustom: + if m.creatingCustom { + protocol := cpProtocols[m.cpProtocolIdx] + return providerTUIResult{ + provider: m.cpNameInput.Value(), + model: m.cpModelInput.Value(), + apiKey: m.apiKeyInput.Value(), + isCustom: true, + url: m.cpURLInput.Value(), + protocol: protocol, + authHeader: m.cpAuthInput.Value(), + } + } + if m.customIdx < len(m.customProviders) { + cp := m.customProviders[m.customIdx] + apiKey := "" + if m.apiKeyMasked { + apiKey = m.apiKeyOriginal + } else { + apiKey = m.apiKeyInput.Value() + } + return providerTUIResult{ + provider: cp.name, + model: cp.entry.Model, + apiKey: apiKey, + isCustom: true, + url: cp.entry.URL, + protocol: cp.entry.Protocol, + authHeader: cp.entry.AuthHeader, + } + } + return providerTUIResult{} + + case tabManual: + return providerTUIResult{ + isManual: true, + url: m.manualURLInput.Value(), + model: m.manualModelInput.Value(), + apiKey: m.manualTokenInput.Value(), + } + } + + return providerTUIResult{} +} + +// --- View --- + +func (m providerTUIModel) View() tea.View { + var s strings.Builder + s.WriteString("\n") + + switch m.step { + case stepProvider: + m.viewProvider(&s) + case stepModel: + m.viewModel(&s) + case stepAPIKey: + m.viewAPIKey(&s) + } + + v := tea.NewView(s.String()) + v.AltScreen = true + return v +} + +func renderTabBar(active providerTab) string { + tabs := []struct { + label string + tab providerTab + }{ + {"Official", tabOfficial}, + {"Custom", tabCustom}, + {"Manual", tabManual}, + } + + var parts []string + for _, t := range tabs { + if t.tab == active { + parts = append(parts, tuiActiveTabStyle.Render("◉ "+t.label)) + } else { + parts = append(parts, tuiInactiveTabStyle.Render("○ "+t.label)) + } + } + return " " + strings.Join(parts, " ") +} + +func (m providerTUIModel) viewProvider(s *strings.Builder) { + s.WriteString(renderTabBar(m.activeTab)) + s.WriteString("\n\n") + + switch m.activeTab { + case tabOfficial: + m.viewOfficialTab(s) + case tabCustom: + m.viewCustomTab(s) + case tabManual: + m.viewManualTab(s) + } + + s.WriteString("\n") + if m.creatingCustom || m.inManualForm { + s.WriteString(tuiHelpStyle.Render(" Enter Confirm · Esc Back")) + } else { + s.WriteString(tuiHelpStyle.Render(" Enter to select · Tab/Arrow keys to navigate · Esc to cancel")) + } + s.WriteString("\n") +} + +func (m providerTUIModel) viewOfficialTab(s *strings.Builder) { + s.WriteString(tuiTitleStyle.Render(" Select a provider")) + s.WriteString("\n\n") + + for i, p := range m.providers { + cursor := " " + if i == m.officialIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + name := p.DisplayName + if i == m.officialIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(name)) + } else { + s.WriteString(cursor + tuiItemStyle.Render(name)) + } + s.WriteString("\n") + } +} + +func (m providerTUIModel) viewCustomTab(s *strings.Builder) { + if m.creatingCustom { + m.viewCustomProviderForm(s) + return + } + + s.WriteString(tuiTitleStyle.Render(" Select a provider")) + s.WriteString("\n\n") + + for i, cp := range m.customProviders { + cursor := " " + if i == m.customIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + label := cp.name + if cp.entry.Model != "" { + label += " " + tuiDimStyle.Render("("+cp.entry.Model+")") + } + if i == m.customIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(cp.name)) + if cp.entry.Model != "" { + s.WriteString(" " + tuiDimStyle.Render("("+cp.entry.Model+")")) + } + } else { + s.WriteString(cursor + label) + } + s.WriteString("\n") + } + + addIdx := len(m.customProviders) + cursor := " " + if m.customIdx == addIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + addLabel := "+ Add custom provider" + if m.customIdx == addIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(addLabel)) + } else { + s.WriteString(cursor + tuiDimStyle.Render(addLabel)) + } + s.WriteString("\n") +} + +func (m providerTUIModel) viewCustomProviderForm(s *strings.Builder) { + s.WriteString(tuiTitleStyle.Render(" Add Custom Provider")) + s.WriteString("\n\n") + + type field struct { + label string + value string + active bool + } + + fields := []field{ + {"Provider name", m.cpNameInput.Value(), m.cpStep == cpStepName}, + {"Protocol", cpProtocols[m.cpProtocolIdx], m.cpStep == cpStepProtocol}, + {"Base URL", m.cpURLInput.Value(), m.cpStep == cpStepBaseURL}, + {"Model", m.cpModelInput.Value(), m.cpStep == cpStepModel}, + {"API Key", strings.Repeat("*", len(m.apiKeyInput.Value())), m.cpStep == cpStepAPIKey}, + {"Auth Header", m.cpAuthInput.Value(), m.cpStep == cpStepAuthHeader}, + } + + for _, f := range fields { + if f.active { + s.WriteString(" " + tuiSelectedItemStyle.Render(f.label+":") + "\n") + switch m.cpStep { + case cpStepName: + s.WriteString(" " + m.cpNameInput.View() + "\n") + case cpStepProtocol: + for i, proto := range cpProtocols { + cur := " " + if i == m.cpProtocolIdx { + cur = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + if i == m.cpProtocolIdx { + s.WriteString(cur + tuiSelectedItemStyle.Render(proto) + "\n") + } else { + s.WriteString(cur + tuiItemStyle.Render(proto) + "\n") + } + } + case cpStepBaseURL: + s.WriteString(" " + m.cpURLInput.View() + "\n") + case cpStepModel: + s.WriteString(" " + m.cpModelInput.View() + "\n") + case cpStepAPIKey: + s.WriteString(" " + m.apiKeyInput.View() + "\n") + case cpStepAuthHeader: + s.WriteString(" " + m.cpAuthInput.View() + "\n") + } + } else if f.value != "" { + s.WriteString(" " + tuiDimStyle.Render(f.label+": "+f.value) + "\n") + } + } +} + +func (m providerTUIModel) viewManualTab(s *strings.Builder) { + if !m.inManualForm { + s.WriteString(tuiTitleStyle.Render(" Manual Configuration")) + s.WriteString("\n\n") + s.WriteString(tuiItemStyle.Render(" Configure LLM endpoint manually.")) + s.WriteString("\n") + if m.existingCfg != nil && m.existingCfg.Llm.URL != "" { + s.WriteString("\n") + s.WriteString(tuiDimStyle.Render(fmt.Sprintf(" Current: %s (%s)", m.existingCfg.Llm.URL, m.existingCfg.Llm.Model))) + s.WriteString("\n") + } + s.WriteString("\n") + s.WriteString(tuiItemStyle.Render(" Press Enter to configure.")) + s.WriteString("\n") + return + } + + s.WriteString(tuiTitleStyle.Render(" Manual Configuration")) + s.WriteString("\n\n") + + type field struct { + label string + value string + active bool + } + + fields := []field{ + {"URL", m.manualURLInput.Value(), m.manualStep == manualStepURL}, + {"Model", m.manualModelInput.Value(), m.manualStep == manualStepModel}, + {"Auth Token", strings.Repeat("*", len(m.manualTokenInput.Value())), m.manualStep == manualStepAuthToken}, + } + + for _, f := range fields { + if f.active { + s.WriteString(" " + tuiSelectedItemStyle.Render(f.label+":") + "\n") + switch m.manualStep { + case manualStepURL: + s.WriteString(" " + m.manualURLInput.View() + "\n") + case manualStepModel: + s.WriteString(" " + m.manualModelInput.View() + "\n") + case manualStepAuthToken: + s.WriteString(" " + m.manualTokenInput.View() + "\n") + } + } else if f.value != "" { + s.WriteString(" " + tuiDimStyle.Render(f.label+": "+f.value) + "\n") + } + } +} + +func (m providerTUIModel) viewModel(s *strings.Builder) { + provider := m.currentProvider() + s.WriteString(tuiTitleStyle.Render(fmt.Sprintf(" Select a model (%s)", provider.DisplayName))) + s.WriteString("\n\n") + + models := m.models() + for i, model := range models { + cursor := " " + if i == m.modelIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + if i == m.modelIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(model)) + } else { + s.WriteString(cursor + tuiItemStyle.Render(model)) + } + s.WriteString("\n") + } + + customIdx := len(models) + cursor := " " + if m.modelIdx == customIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + customLabel := "Enter custom model name..." + if m.modelIdx == customIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(customLabel)) + } else { + s.WriteString(cursor + tuiDimStyle.Render(customLabel)) + } + s.WriteString("\n") + + if m.customModel { + s.WriteString("\n") + s.WriteString(" " + m.modelInput.View()) + s.WriteString("\n") + } + + s.WriteString("\n") + s.WriteString(tuiHelpStyle.Render(" ↑/↓ Select Enter Confirm Esc Back")) + s.WriteString("\n") +} + +func (m providerTUIModel) viewAPIKey(s *strings.Builder) { + var title string + if m.activeTab == tabCustom && m.customIdx < len(m.customProviders) { + title = fmt.Sprintf(" Enter API Key (%s)", m.customProviders[m.customIdx].name) + } else { + provider := m.currentProvider() + title = fmt.Sprintf(" Enter API Key (%s)", provider.DisplayName) + } + s.WriteString(tuiTitleStyle.Render(title)) + s.WriteString("\n\n") + + s.WriteString(" " + m.apiKeyInput.View()) + s.WriteString("\n") + + if m.activeTab == tabOfficial { + provider := m.currentProvider() + if envKey := os.Getenv(provider.EnvVar); envKey != "" { + s.WriteString("\n") + s.WriteString(tuiDimStyle.Render(fmt.Sprintf(" $%s is set", provider.EnvVar))) + s.WriteString("\n") + } else { + s.WriteString("\n") + s.WriteString(tuiDimStyle.Render(fmt.Sprintf(" Tip: You can also set via env var %s", provider.EnvVar))) + s.WriteString("\n") + } + } + + s.WriteString("\n") + s.WriteString(tuiHelpStyle.Render(" Enter Confirm Esc Back")) + s.WriteString("\n") +} + +// --- Styles --- + +const tuiCursor = "▸" + +var ( + tuiTitleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("15")) + + tuiCursorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("12")) + + tuiSelectedItemStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("12")) + + tuiItemStyle = lipgloss.NewStyle() + + tuiDimStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("8")) + + tuiHelpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("8")) + + tuiActiveTabStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("12")) + + tuiInactiveTabStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("8")) +) + +// --- Model-only TUI (for `ocr config model`) --- + +type modelTUIModel struct { + width int + height int + + provider llm.Provider + models []string + modelIdx int + customModel bool + modelInput textinput.Model + + confirmed bool + cancelled bool +} + +func newModelTUI(provider llm.Provider, currentModel string) modelTUIModel { + mi := textinput.New() + mi.Placeholder = "model name" + mi.SetWidth(40) + + m := modelTUIModel{ + provider: provider, + models: provider.Models, + width: 80, + height: 24, + modelInput: mi, + } + + if currentModel != "" { + found := false + for i, model := range provider.Models { + if model == currentModel { + m.modelIdx = i + found = true + break + } + } + if !found { + m.modelIdx = len(provider.Models) + m.modelInput.SetValue(currentModel) + } + } + + return m +} + +func (m modelTUIModel) Init() tea.Cmd { + return nil +} + +func (m modelTUIModel) isCustomItem(idx int) bool { + return idx == len(m.models) +} + +func (m modelTUIModel) itemCount() int { + return len(m.models) + 1 +} + +func (m modelTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + + case tea.KeyPressMsg: + key := msg.String() + + if m.customModel { + switch key { + case "esc": + m.customModel = false + m.modelInput.Blur() + m.modelInput.SetValue("") + return m, nil + case "enter": + if m.modelInput.Value() != "" { + m.confirmed = true + return m, tea.Quit + } + return m, nil + default: + var cmd tea.Cmd + m.modelInput, cmd = m.modelInput.Update(msg) + return m, cmd + } + } + + switch key { + case "ctrl+c", "esc": + m.cancelled = true + return m, tea.Quit + case "enter": + if m.isCustomItem(m.modelIdx) { + m.customModel = true + return m, m.modelInput.Focus() + } + m.confirmed = true + return m, tea.Quit + case "up", "k": + if m.modelIdx > 0 { + m.modelIdx-- + } + return m, nil + case "down", "j": + if m.modelIdx < m.itemCount()-1 { + m.modelIdx++ + } + return m, nil + } + + default: + if m.customModel { + var cmd tea.Cmd + m.modelInput, cmd = m.modelInput.Update(msg) + return m, cmd + } + } + return m, nil +} + +func (m modelTUIModel) selectedModel() string { + if m.customModel || m.isCustomItem(m.modelIdx) { + return m.modelInput.Value() + } + if m.modelIdx < len(m.models) { + return m.models[m.modelIdx] + } + return "" +} + +func (m modelTUIModel) View() tea.View { + var s strings.Builder + s.WriteString("\n") + s.WriteString(tuiTitleStyle.Render(fmt.Sprintf(" Select a model (%s)", m.provider.DisplayName))) + s.WriteString("\n\n") + + for i, model := range m.models { + cursor := " " + if i == m.modelIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + if i == m.modelIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(model)) + } else { + s.WriteString(cursor + tuiItemStyle.Render(model)) + } + s.WriteString("\n") + } + + customIdx := len(m.models) + cursor := " " + if m.modelIdx == customIdx { + cursor = " " + tuiCursorStyle.Render(tuiCursor) + " " + } + customLabel := "Enter custom model name..." + if m.modelIdx == customIdx { + s.WriteString(cursor + tuiSelectedItemStyle.Render(customLabel)) + } else { + s.WriteString(cursor + tuiDimStyle.Render(customLabel)) + } + s.WriteString("\n") + + if m.customModel { + s.WriteString("\n") + s.WriteString(" " + m.modelInput.View()) + s.WriteString("\n") + } + + s.WriteString("\n") + s.WriteString(tuiHelpStyle.Render(" ↑/↓ Select Enter Confirm Esc Cancel")) + s.WriteString("\n") + + v := tea.NewView(s.String()) + v.AltScreen = true + return v +} diff --git a/cmd/opencodereview/provider_tui_test.go b/cmd/opencodereview/provider_tui_test.go new file mode 100644 index 0000000..2dc6ab4 --- /dev/null +++ b/cmd/opencodereview/provider_tui_test.go @@ -0,0 +1,514 @@ +package main + +import ( + "testing" + + tea "charm.land/bubbletea/v2" +) + +func escKey() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyEscape} +} + +func enterKey() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyEnter} +} + +func leftKey() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyLeft} +} + +func rightKey() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyRight} +} + +func downKey() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyDown} +} + +func tabKeyMsg() tea.KeyPressMsg { + return tea.KeyPressMsg{Code: tea.KeyTab} +} + +func charKey(c rune) tea.KeyPressMsg { + return tea.KeyPressMsg{Code: c} +} + +// --- Tab switching tests --- + +func TestProviderTUI_TabSwitchRight(t *testing.T) { + m := newProviderTUI(&Config{}) + if m.activeTab != tabOfficial { + t.Fatalf("initial tab = %d, want %d", m.activeTab, tabOfficial) + } + + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + if m2.activeTab != tabCustom { + t.Errorf("after right, tab = %d, want %d", m2.activeTab, tabCustom) + } + + result, _ = m2.Update(rightKey()) + m3 := result.(providerTUIModel) + if m3.activeTab != tabManual { + t.Errorf("after 2x right, tab = %d, want %d", m3.activeTab, tabManual) + } + + // Should not go past last tab + result, _ = m3.Update(rightKey()) + m4 := result.(providerTUIModel) + if m4.activeTab != tabManual { + t.Errorf("after 3x right, tab = %d, want %d (should clamp)", m4.activeTab, tabManual) + } +} + +func TestProviderTUI_TabSwitchLeft(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Go to manual tab first + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + result, _ = m2.Update(rightKey()) + m3 := result.(providerTUIModel) + if m3.activeTab != tabManual { + t.Fatalf("setup: tab = %d, want %d", m3.activeTab, tabManual) + } + + result, _ = m3.Update(leftKey()) + m4 := result.(providerTUIModel) + if m4.activeTab != tabCustom { + t.Errorf("after left, tab = %d, want %d", m4.activeTab, tabCustom) + } + + result, _ = m4.Update(leftKey()) + m5 := result.(providerTUIModel) + if m5.activeTab != tabOfficial { + t.Errorf("after 2x left, tab = %d, want %d", m5.activeTab, tabOfficial) + } + + // Should not go past first tab + result, _ = m5.Update(leftKey()) + m6 := result.(providerTUIModel) + if m6.activeTab != tabOfficial { + t.Errorf("after 3x left, tab = %d, want %d (should clamp)", m6.activeTab, tabOfficial) + } +} + +func TestProviderTUI_TabKeyCycles(t *testing.T) { + m := newProviderTUI(&Config{}) + + result, _ := m.Update(tabKeyMsg()) + m2 := result.(providerTUIModel) + if m2.activeTab != tabCustom { + t.Errorf("after tab, tab = %d, want %d", m2.activeTab, tabCustom) + } + + result, _ = m2.Update(tabKeyMsg()) + m3 := result.(providerTUIModel) + if m3.activeTab != tabManual { + t.Errorf("after 2x tab, tab = %d, want %d", m3.activeTab, tabManual) + } + + result, _ = m3.Update(tabKeyMsg()) + m4 := result.(providerTUIModel) + if m4.activeTab != tabOfficial { + t.Errorf("after 3x tab, tab = %d, want %d (should wrap)", m4.activeTab, tabOfficial) + } +} + +func TestProviderTUI_TabSwitchOnlyOnStepProvider(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Advance to stepModel + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + if m2.step != stepModel { + t.Fatalf("step = %d, want %d", m2.step, stepModel) + } + + // Tab keys should not change tab + result, _ = m2.Update(rightKey()) + m3 := result.(providerTUIModel) + if m3.activeTab != tabOfficial { + t.Errorf("right on stepModel should not change tab: got %d", m3.activeTab) + } +} + +// --- Official tab tests (updated from original) --- + +func TestProviderTUI_EscFromModelGoesBackToProvider(t *testing.T) { + m := newProviderTUI(&Config{}) + + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + if m2.step != stepModel { + t.Fatalf("after Enter, step = %d, want %d (stepModel)", m2.step, stepModel) + } + + result, _ = m2.Update(escKey()) + m3 := result.(providerTUIModel) + if m3.step != stepProvider { + t.Errorf("after Esc on stepModel, step = %d, want %d (stepProvider)", m3.step, stepProvider) + } + if m3.cancelled { + t.Error("should not be cancelled when going back from stepModel") + } +} + +func TestProviderTUI_EscFromAPIKeyGoesBackToModel(t *testing.T) { + m := newProviderTUI(&Config{}) + + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + + result, _ = m2.Update(enterKey()) + m3 := result.(providerTUIModel) + if m3.step != stepAPIKey { + t.Fatalf("after 2x Enter, step = %d, want %d (stepAPIKey)", m3.step, stepAPIKey) + } + + result, _ = m3.Update(escKey()) + m4 := result.(providerTUIModel) + if m4.step != stepModel { + t.Errorf("after Esc on stepAPIKey, step = %d, want %d (stepModel)", m4.step, stepModel) + } +} + +func TestProviderTUI_EscFromProviderCancels(t *testing.T) { + m := newProviderTUI(&Config{}) + + result, cmd := m.Update(escKey()) + m2 := result.(providerTUIModel) + if !m2.cancelled { + t.Error("Esc on stepProvider should set cancelled = true") + } + if cmd == nil { + t.Error("Esc on stepProvider should return tea.Quit") + } +} + +func TestProviderTUI_EscKeyString(t *testing.T) { + esc := escKey() + if s := esc.String(); s != "esc" { + t.Errorf("escape key String() = %q, want %q", s, "esc") + } +} + +// --- Manual tab tests --- + +func TestProviderTUI_ManualTabEnterStartsForm(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Switch to manual tab + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + result, _ = m2.Update(rightKey()) + m3 := result.(providerTUIModel) + if m3.activeTab != tabManual { + t.Fatalf("tab = %d, want %d", m3.activeTab, tabManual) + } + + // Press Enter to start form + result, _ = m3.Update(enterKey()) + m4 := result.(providerTUIModel) + if !m4.inManualForm { + t.Error("Enter on manual tab should set inManualForm = true") + } + if m4.manualStep != manualStepURL { + t.Errorf("manualStep = %d, want %d", m4.manualStep, manualStepURL) + } +} + +func TestProviderTUI_ManualFormEscFromURLExitsForm(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Switch to manual tab and enter form + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + result, _ = m2.Update(rightKey()) + m3 := result.(providerTUIModel) + result, _ = m3.Update(enterKey()) + m4 := result.(providerTUIModel) + if !m4.inManualForm { + t.Fatalf("should be in manual form") + } + + // Esc should exit form, not cancel + result, _ = m4.Update(escKey()) + m5 := result.(providerTUIModel) + if m5.inManualForm { + t.Error("Esc from URL step should exit form") + } + if m5.cancelled { + t.Error("should not be cancelled when exiting form") + } +} + +func TestProviderTUI_ManualFormEscRestoresOriginalValues(t *testing.T) { + cfg := &Config{ + Llm: LlmConfig{ + URL: "https://example.com/v1", + Model: "test-model", + AuthToken: "token-123", + }, + } + m := newProviderTUI(cfg) + + // Enter the form + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + if !m2.inManualForm { + t.Fatalf("should be in manual form") + } + + // Simulate editing by directly modifying the input value + m2.manualURLInput.SetValue("https://modified.example.com") + + // Esc should restore original values + result, _ = m2.Update(escKey()) + m3 := result.(providerTUIModel) + if m3.inManualForm { + t.Error("should have exited form") + } + if m3.manualURLInput.Value() != "https://example.com/v1" { + t.Errorf("URL not restored: got %q, want %q", m3.manualURLInput.Value(), "https://example.com/v1") + } + if m3.manualModelInput.Value() != "test-model" { + t.Errorf("Model not restored: got %q, want %q", m3.manualModelInput.Value(), "test-model") + } + if m3.manualTokenInput.Value() != "token-123" { + t.Errorf("Token not restored: got %q, want %q", m3.manualTokenInput.Value(), "token-123") + } +} + +func TestProviderTUI_ManualFormPrefilledValues(t *testing.T) { + cfg := &Config{ + Llm: LlmConfig{ + URL: "https://example.com/v1", + Model: "test-model", + AuthToken: "token-123", + }, + } + m := newProviderTUI(cfg) + + if m.activeTab != tabManual { + t.Fatalf("should auto-select manual tab when Llm.URL is set, got %d", m.activeTab) + } + if m.manualURLInput.Value() != "https://example.com/v1" { + t.Errorf("URL not prefilled: got %q", m.manualURLInput.Value()) + } + if m.manualModelInput.Value() != "test-model" { + t.Errorf("Model not prefilled: got %q", m.manualModelInput.Value()) + } + if m.manualTokenInput.Value() != "token-123" { + t.Errorf("Token not prefilled: got %q", m.manualTokenInput.Value()) + } +} + +func TestProviderTUI_ManualResult(t *testing.T) { + cfg := &Config{ + Llm: LlmConfig{ + URL: "https://example.com/v1", + Model: "test-model", + AuthToken: "token-123", + }, + } + m := newProviderTUI(cfg) + + // Enter the form + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + m2.confirmed = true + + r := m2.result() + if !r.isManual { + t.Error("result should have isManual = true") + } + if r.url != "https://example.com/v1" { + t.Errorf("result url = %q, want %q", r.url, "https://example.com/v1") + } + if r.model != "test-model" { + t.Errorf("result model = %q, want %q", r.model, "test-model") + } +} + +func TestProviderTUI_ManualFormPrefilledWhenProviderSet(t *testing.T) { + cfg := &Config{ + Provider: "my-gateway", + CustomProviders: map[string]ProviderEntry{ + "my-gateway": {URL: "https://gw.example.com/v1", Protocol: "openai", Model: "llama-3"}, + }, + Llm: LlmConfig{ + URL: "https://manual.example.com/v1", + Model: "manual-model", + AuthToken: "manual-token", + }, + } + m := newProviderTUI(cfg) + + if m.activeTab != tabCustom { + t.Fatalf("should auto-select custom tab, got %d", m.activeTab) + } + if m.manualURLInput.Value() != "https://manual.example.com/v1" { + t.Errorf("URL not prefilled: got %q", m.manualURLInput.Value()) + } + if m.manualModelInput.Value() != "manual-model" { + t.Errorf("Model not prefilled: got %q", m.manualModelInput.Value()) + } + if m.manualTokenInput.Value() != "manual-token" { + t.Errorf("Token not prefilled: got %q", m.manualTokenInput.Value()) + } +} + +// --- Custom tab tests --- + +func TestProviderTUI_CustomTabShowsAddOption(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Switch to custom tab + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + if m2.activeTab != tabCustom { + t.Fatalf("tab = %d, want %d", m2.activeTab, tabCustom) + } + + // With no custom providers, only "Add" option exists at index 0 + if m2.customListCount() != 1 { + t.Errorf("customListCount() = %d, want 1 (only add option)", m2.customListCount()) + } +} + +func TestProviderTUI_CustomTabSelectAddStartsForm(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Switch to custom tab + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + + // Enter on "Add" option + result, _ = m2.Update(enterKey()) + m3 := result.(providerTUIModel) + if !m3.creatingCustom { + t.Error("Enter on add option should set creatingCustom = true") + } + if m3.cpStep != cpStepName { + t.Errorf("cpStep = %d, want %d", m3.cpStep, cpStepName) + } +} + +func TestProviderTUI_CustomFormEscFromNameExitsForm(t *testing.T) { + m := newProviderTUI(&Config{}) + + // Switch to custom tab and start form + result, _ := m.Update(rightKey()) + m2 := result.(providerTUIModel) + result, _ = m2.Update(enterKey()) + m3 := result.(providerTUIModel) + if !m3.creatingCustom { + t.Fatalf("should be creating custom") + } + + // Esc from name step should exit form + result, _ = m3.Update(escKey()) + m4 := result.(providerTUIModel) + if m4.creatingCustom { + t.Error("Esc from name step should exit custom form") + } + if m4.cancelled { + t.Error("should not be cancelled") + } +} + +func TestProviderTUI_CustomProviderExistsInList(t *testing.T) { + cfg := &Config{ + Provider: "my-llm", + CustomProviders: map[string]ProviderEntry{ + "my-llm": { + URL: "https://custom.api/v1", + Protocol: "openai", + Model: "custom-model", + APIKey: "key-123", + }, + }, + } + m := newProviderTUI(cfg) + + if m.activeTab != tabCustom { + t.Fatalf("should auto-select custom tab, got %d", m.activeTab) + } + if len(m.customProviders) != 1 { + t.Fatalf("expected 1 custom provider, got %d", len(m.customProviders)) + } + if m.customProviders[0].name != "my-llm" { + t.Errorf("custom provider name = %q, want %q", m.customProviders[0].name, "my-llm") + } +} + +func TestProviderTUI_SelectExistingCustomGoesToAPIKey(t *testing.T) { + cfg := &Config{ + Provider: "my-llm", + CustomProviders: map[string]ProviderEntry{ + "my-llm": { + URL: "https://custom.api/v1", + Protocol: "openai", + Model: "custom-model", + APIKey: "key-123", + }, + }, + } + m := newProviderTUI(cfg) + + // Enter on existing custom provider should go to API key step + result, _ := m.Update(enterKey()) + m2 := result.(providerTUIModel) + if m2.step != stepAPIKey { + t.Errorf("step = %d, want %d (stepAPIKey)", m2.step, stepAPIKey) + } +} + +// --- collectCustomProviders tests --- + +func TestCollectCustomProviders_NilConfig(t *testing.T) { + result := collectCustomProviders(nil) + if result != nil { + t.Errorf("expected nil, got %v", result) + } +} + +func TestCollectCustomProviders_ReadsCustomProviders(t *testing.T) { + cfg := &Config{ + Providers: map[string]ProviderEntry{ + "anthropic": {APIKey: "key1"}, + "openai": {APIKey: "key2"}, + }, + CustomProviders: map[string]ProviderEntry{ + "my-custom": {URL: "https://example.com", Protocol: "openai"}, + }, + } + result := collectCustomProviders(cfg) + if len(result) != 1 { + t.Fatalf("expected 1 custom provider, got %d", len(result)) + } + if result[0].name != "my-custom" { + t.Errorf("name = %q, want %q", result[0].name, "my-custom") + } +} + +func TestCollectCustomProviders_SortedByName(t *testing.T) { + cfg := &Config{ + CustomProviders: map[string]ProviderEntry{ + "zzz-provider": {URL: "https://z.example.com"}, + "aaa-provider": {URL: "https://a.example.com"}, + }, + } + result := collectCustomProviders(cfg) + if len(result) != 2 { + t.Fatalf("expected 2, got %d", len(result)) + } + if result[0].name != "aaa-provider" { + t.Errorf("first = %q, want %q", result[0].name, "aaa-provider") + } + if result[1].name != "zzz-provider" { + t.Errorf("second = %q, want %q", result[1].name, "zzz-provider") + } +} diff --git a/go.mod b/go.mod index 0c9b1f3..c6dcda1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/open-code-review/open-code-review go 1.25.0 require ( + charm.land/bubbles/v2 v2.1.0 + charm.land/bubbletea/v2 v2.0.7 + charm.land/lipgloss/v2 v2.0.3 github.com/anthropics/anthropic-sdk-go v1.47.0 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/openai/openai-go/v3 v3.39.0 @@ -19,29 +22,43 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/invopop/jsonschema v0.14.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index c28e891..291ab0b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,15 @@ +charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= +charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= +charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0= +charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs= +charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= +charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= github.com/anthropics/anthropic-sdk-go v1.47.0 h1:p1F48S/5UAGK3h2NzvZP8rqKnZqB7RkyYvOEM8dOEaQ= github.com/anthropics/anthropic-sdk-go v1.47.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= @@ -10,6 +20,24 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek= +github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= @@ -31,6 +59,12 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/openai/openai-go/v3 v3.39.0 h1:WgLGgMOOdQDkZyo8YIhzUNXRXlEc+OJfU4EKP5Qp6AA= github.com/openai/openai-go/v3 v3.39.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= @@ -39,6 +73,8 @@ github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/Q github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI= github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -53,6 +89,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -81,12 +119,14 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/imgs/providers.jpg b/imgs/providers.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed03ddbccd29ed97d1a145e663e392173c53edec GIT binary patch literal 97846 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
    h*U5jREApp>4mP|{!KaS6O;538w1E=d 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 fTyPB` { codeBlocks: [ { label: t('quickstart.step2Label1'), + code: `ocr config provider` + }, + { + label: t('quickstart.step2Label2'), code: `ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true` }, - { label: t('quickstart.step2Label2'), code: `ocr config set language Chinese` }, { label: t('quickstart.step2Label3'), code: `ocr llm test` } ] }, diff --git a/pages/src/i18n/en.ts b/pages/src/i18n/en.ts index 10176d2..d4401f4 100644 --- a/pages/src/i18n/en.ts +++ b/pages/src/i18n/en.ts @@ -82,9 +82,9 @@ export const en: TranslationKeys = { 'quickstart.step1Label1': 'Install', 'quickstart.step1Label2': 'Verify Installation', 'quickstart.step2Title': 'Configure', - 'quickstart.step2Desc': 'Configure your LLM info. Supports Anthropic and OpenAI-compatible protocols. Config file at ~/.opencodereview/config.json.', - 'quickstart.step2Label1': 'Configure LLM', - 'quickstart.step2Label2': 'Configure Language (default English)', + 'quickstart.step2Desc': 'Set up your LLM provider interactively, or configure manually. Built-in support for Anthropic, OpenAI, DashScope, DeepSeek, and Z.AI.', + 'quickstart.step2Label1': 'Interactive Setup (Recommended)', + 'quickstart.step2Label2': 'Manual Config (Alternative)', 'quickstart.step2Label3': 'Verify Configuration', 'quickstart.step3Title': 'Run Review', 'quickstart.step3Desc': 'Start your first code review.', @@ -117,6 +117,13 @@ export const en: TranslationKeys = { 'docs.configTitle': 'Configuration & Verification', 'docs.configDesc': 'Manage CLI configuration settings, stored in ~/.opencodereview/config.json.', + 'docs.configInteractive': 'Interactive Setup (Recommended)', + 'docs.configInteractiveDesc': 'The easiest way to configure Open Code Review. Select from built-in providers or add a custom one.', + 'docs.configModelSelect': 'Model Selection', + 'docs.configModelSelectDesc': 'After choosing a provider, pick the model to use.', + 'docs.configListProviders': 'List Built-in Providers', + 'docs.configListProvidersDesc': 'View all available built-in LLM providers.', + 'docs.configManual': 'Manual Configuration', 'docs.configCommand': 'Command', 'docs.configExample': 'Example', 'docs.configKeys': 'Supported Config Keys', diff --git a/pages/src/i18n/zh.ts b/pages/src/i18n/zh.ts index 026872b..62ad27c 100644 --- a/pages/src/i18n/zh.ts +++ b/pages/src/i18n/zh.ts @@ -82,9 +82,9 @@ export const zh: TranslationKeys = { 'quickstart.step1Label1': '安装', 'quickstart.step1Label2': '验证安装', 'quickstart.step2Title': '配置', - 'quickstart.step2Desc': '配置您的 LLM 信息,支持 Anthropic 和 OpenAI 兼容协议。配置文件位于 ~/.opencodereview/config.json。', - 'quickstart.step2Label1': '配置 LLM', - 'quickstart.step2Label2': '配置语言(默认英文)', + 'quickstart.step2Desc': '通过交互式向导设置 LLM 供应商,或手动配置。内置支持 Anthropic、OpenAI、DashScope、DeepSeek 和 Z.AI。', + 'quickstart.step2Label1': '交互式设置(推荐)', + 'quickstart.step2Label2': '手动配置(备选)', 'quickstart.step2Label3': '验证配置', 'quickstart.step3Title': '运行评审', 'quickstart.step3Desc': '开始你的第一次代码评审。', @@ -117,6 +117,13 @@ export const zh: TranslationKeys = { 'docs.configTitle': '配置与验证', 'docs.configDesc': '管理 CLI 的配置设置,存储在 ~/.opencodereview/config.json。', + 'docs.configInteractive': '交互式设置(推荐)', + 'docs.configInteractiveDesc': '最简单的配置方式。从内置供应商中选择,或添加自定义供应商。', + 'docs.configModelSelect': '模型选择', + 'docs.configModelSelectDesc': '选择供应商后,选择要使用的模型。', + 'docs.configListProviders': '查看内置供应商', + 'docs.configListProvidersDesc': '查看所有可用的内置 LLM 供应商。', + 'docs.configManual': '手动配置', 'docs.configCommand': '命令', 'docs.configExample': '示例', 'docs.configKeys': '支持的配置 Key', diff --git a/pages/src/pages/DocsPage.tsx b/pages/src/pages/DocsPage.tsx index c52abe3..150ee4d 100644 --- a/pages/src/pages/DocsPage.tsx +++ b/pages/src/pages/DocsPage.tsx @@ -227,10 +227,43 @@ const DocsPage: React.FC = () => {

    -

    {t('docs.configCommand')}

    +

    {t('docs.configInteractive')}

    +

    {t('docs.configInteractiveDesc')}

    +
    + handleCopy('ocr config provider', 'config-provider')} + copyLabel={t('docs.copy')} + /> +
    +
    +

    {t('docs.configModelSelect')}

    +

    {t('docs.configModelSelectDesc')}

    + handleCopy('ocr config model', 'config-model')} + copyLabel={t('docs.copy')} + /> +
    +
    +

    {t('docs.configListProviders')}

    +

    {t('docs.configListProvidersDesc')}

    + handleCopy('ocr llm providers', 'config-providers-list')} + copyLabel={t('docs.copy')} + /> +
    + +

    {t('docs.configManual')}

    + +

    {t('docs.configCommand')}

    -

    {t('docs.configExample')}

    +

    {t('docs.configExample')}