From d2c49ed2ed098f8fb398b5ba60abaee4aa304b88 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Sep 2026 16:37:14 +0800 Subject: [PATCH 1/4] phi config: replace HTML editor with full-screen TUI Replace the loopback web server + embedded HTML page with a keyboard-driven full-screen terminal editor (internal/tui/configui). The form edits ~/.phi/config.yaml with inline text fields, choice pickers (model list, thinking level, api, permission mode), tri-state toggles, and expandable bash allow/deny rule lists. Nothing is written to disk until the user presses s; the previous file is kept as config.yaml.bak. The editable schema moved to internal/project (project.ConfigDoc, ReadConfigDoc, Save, Validate) so the editor and the runtime parser share one definition. Pointer fields preserve the "key absent vs zero value" distinction the loader relies on, and empty api_key/base_url values are omitted from saved files. The old cmd/config.html, the HTTP server, origin checks, and the browser launch are gone. fetchModelIDs stays as a standalone function that probes OpenAI/Anthropic /models endpoints with redirect-same-origin protection and dual-envelope decoding. README/README.zh-CN updated with TUI key bindings; stale screenshot removed. CHANGELOG updated under Changed + Removed. --- CHANGELOG.md | 14 + README.md | 16 +- README.zh-CN.md | 13 +- assets/config.png | Bin 160403 -> 0 bytes cmd/config.go | 341 +---- cmd/config.html | 1866 -------------------------- cmd/config_cmd.go | 66 +- cmd/config_test.go | 451 ++----- cmd/main.go | 2 +- internal/project/config.go | 87 +- internal/project/configdoc.go | 156 +++ internal/project/configdoc_test.go | 152 +++ internal/tui/configui/doc.go | 14 + internal/tui/configui/editor.go | 822 ++++++++++++ internal/tui/configui/editor_test.go | 558 ++++++++ internal/tui/configui/model.go | 921 +++++++++++++ internal/tui/configui/render.go | 380 ++++++ 17 files changed, 3231 insertions(+), 2628 deletions(-) delete mode 100644 assets/config.png delete mode 100644 cmd/config.html create mode 100644 internal/project/configdoc.go create mode 100644 internal/project/configdoc_test.go create mode 100644 internal/tui/configui/doc.go create mode 100644 internal/tui/configui/editor.go create mode 100644 internal/tui/configui/editor_test.go create mode 100644 internal/tui/configui/model.go create mode 100644 internal/tui/configui/render.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e2aea0a5..8b9862ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,24 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed +- `phi config` is now a full-screen terminal editor (`internal/tui/configui`) + instead of a loopback web page. The form edits the same keys with inline text + fields, choice pickers (model list, thinking level, api, permission mode), + tri-state toggles, and expandable bash allow/deny rule lists. Nothing is + written until a save, and the previous file is kept as `config.yaml.bak`. +- The editable config schema moved to `internal/project` (`project.ConfigDoc`, + `ReadConfigDoc`, `Save`, `Validate`). The runtime parser and the editor now + share one definition instead of two hand-kept copies, and empty `api_key` / + `base_url` values are omitted when a file is written. + ### Deprecated ### Removed +- The embedded HTML config page and its `/api/config` and `/api/models` + endpoints, along with the local HTTP server, origin checks, and browser + launch that served them. + ### Fixed - `diff` pane notes no longer persist. `i` / `x` / `a` keep working, but drafts diff --git a/README.md b/README.md index ac5dffa6..ead257b3 100644 --- a/README.md +++ b/README.md @@ -136,10 +136,20 @@ figures. ## Configuration phi reads `~/.phi/config.yaml` (standard YAML). Environment variables -override it for one-off runs. `phi config` opens an HTML editor for the same -file in your browser. +override it for one-off runs. `phi config` opens a full-screen terminal editor +for the same file — nothing is written until you save, and the previous file is +kept as `config.yaml.bak`. -![phi config](assets/config.png) +``` +phi config keys + ↑↓ ←→ move / cycle a value a add a model + ⏎ edit or open a picker d delete the focused row + esc close, or quit f fetch model ids from the provider + ^s save s save q quit +``` + +An empty field means "not set", i.e. the value the loader would fill in; the +placeholder shows that value. ```yaml # ~/.phi/config.yaml diff --git a/README.zh-CN.md b/README.zh-CN.md index d0e1eccd..b5b30018 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -136,9 +136,18 @@ TUI 给模型提供四个核心工具——`read`、`write`、`edit` 和 `bash` ## 配置 phi 读取 `~/.phi/config.yaml`(标准 YAML)。环境变量可覆盖配置,用于一次性运行。 -`phi config` 会在浏览器中打开一个 HTML 编辑器来编辑同一个文件。 +`phi config` 会在终端里打开全屏编辑器来编辑同一个文件;不保存就不落盘,保存前 +的旧文件会留作 `config.yaml.bak`。 -![phi config](assets/config.png) +``` +phi config 按键 + ↑↓ ←→ 移动 / 切换取值 a 新增模型 + ⏎ 编辑,或打开选择列表 d 删除当前行 + esc 收起列表 / 退出 f 从服务商拉取模型列表 + ^s 保存 s 保存 q 退出 +``` + +留空表示「未设置」,也就是交给加载器填默认值;占位符显示的就是那个值。 ```yaml # ~/.phi/config.yaml diff --git a/assets/config.png b/assets/config.png deleted file mode 100644 index df4d59c7a0a71adffe1941166b93e696ff6783f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160403 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 z 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

d7 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 diff --git a/cmd/config.go b/cmd/config.go index 0198df27..cbd6e0a9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,89 +2,29 @@ package main import ( "context" - _ "embed" "encoding/json" "errors" "fmt" "io" - "mime" - "net" "net/http" "net/url" - "os" - "os/exec" - "runtime" "sort" "strings" "time" - "gopkg.in/yaml.v3" - "github.com/pulseaiclub/phi/internal/util" ) -//go:embed config.html -var configHTML []byte - -// configDoc is the document served to / accepted from the config editor. -// The yaml tags mirror internal/project's fileConfig so the page round-trips -// config.yaml losslessly for every key the parser understands; the json tags -// drive the editor API. Pointer fields preserve "key absent" across saves so -// untouched sections are never rewritten. -type configDoc struct { - Path string `yaml:"-" json:"path,omitempty"` - Models []modelDoc `yaml:"models" json:"models"` - SkillPath *string `yaml:"skill_path,omitempty" json:"skillPath,omitempty"` - Permissions *permDoc `yaml:"permissions,omitempty" json:"permissions,omitempty"` - Agents *agentsDoc `yaml:"agents,omitempty" json:"agents,omitempty"` -} - -type modelDoc struct { - Name string `yaml:"name" json:"name"` - APIKey string `yaml:"api_key" json:"apiKey"` - BaseURL string `yaml:"base_url" json:"baseUrl"` - ContextWindow *int `yaml:"context_window,omitempty" json:"contextWindow,omitempty"` - // ImageEnabled is a pointer so the editor can omit the key until the user - // toggles it (absent vs false). Runtime parse treats absence as false. - ImageEnabled *bool `yaml:"image_enabled,omitempty" json:"imageEnabled,omitempty"` - API string `yaml:"api,omitempty" json:"api,omitempty"` // OpenAI | OpenAIResponses | Anthropic | Gemini - ThinkEnabled *bool `yaml:"think_enabled,omitempty" json:"thinkEnabled,omitempty"` - ThinkLevel string `yaml:"think_level,omitempty" json:"thinkLevel,omitempty"` - Default bool `yaml:"default,omitempty" json:"default"` -} - -type permDoc struct { - Mode *string `yaml:"mode,omitempty" json:"mode,omitempty"` - WorkspaceOnlyWrites *bool `yaml:"workspace_only_writes,omitempty" json:"workspaceOnlyWrites,omitempty"` - AskTimeoutSec *int `yaml:"ask_timeout_sec,omitempty" json:"askTimeoutSec,omitempty"` - DangerouslyAllowAll *bool `yaml:"dangerously_allow_all,omitempty" json:"dangerouslyAllowAll,omitempty"` - Bash *bashDoc `yaml:"bash,omitempty" json:"bash,omitempty"` -} - -type bashDoc struct { - Default *string `yaml:"default,omitempty" json:"default,omitempty"` - Allow []string `yaml:"allow" json:"allow,omitempty"` - Deny []string `yaml:"deny" json:"deny,omitempty"` -} - -type agentsDoc struct { - // Enabled is a pointer so omitting the key in YAML/JSON keeps default-on - // when only agents.models is set. - Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` - Models *agentsModelsDoc `yaml:"models,omitempty" json:"models,omitempty"` -} - -type agentsModelsDoc struct { - Explore string `yaml:"explore,omitempty" json:"explore,omitempty"` - Review string `yaml:"review,omitempty" json:"review,omitempty"` - Worker string `yaml:"worker,omitempty" json:"worker,omitempty"` -} +const ( + defaultOpenAIBaseURL = "https://api.openai.com/v1" + anthropicAPIVersion = "2023-06-01" + modelListTimeout = 15 * time.Second + modelListBodyLimit = int64(4 << 20) +) -type modelListRequest struct { - BaseURL string `json:"baseUrl"` - APIKey string `json:"apiKey"` - Model string `json:"model"` - API string `json:"api"` // OpenAI | OpenAIResponses | Anthropic | Gemini; empty falls back to legacy heuristics +type modelListResponse struct { + Data []modelListItem `json:"data"` + Models []modelListItem `json:"models"` } type modelListItem struct { @@ -93,97 +33,12 @@ type modelListItem struct { DisplayName string `json:"display_name"` } -type modelListResponse struct { - Data []modelListItem `json:"data"` - Models []modelListItem `json:"models"` -} - -const ( - defaultOpenAIBaseURL = "https://api.openai.com/v1" - anthropicAPIVersion = "2023-06-01" - modelListRequestLimit = 15 * time.Second - modelListBodyLimit = int64(4 << 20) -) - -// configHandler serves the embedded editor page and its /api/config endpoints. -type configHandler struct { - configPath string -} - -func (h *configHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if (r.URL.Path == "/api/config" || r.URL.Path == "/api/models") && !isLoopbackHost(r.Host) { - writeConfigErr(w, http.StatusForbidden, errors.New("request origin is not allowed")) - return - } - - switch r.URL.Path { - case "/": - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = w.Write(configHTML) - case "/api/config": - h.handleConfig(w, r) - case "/api/models": - h.handleModels(w, r) - default: - http.NotFound(w, r) - } -} - -func (h *configHandler) handleConfig(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - doc, err := readConfigDoc(h.configPath) - if err != nil { - writeConfigErr(w, http.StatusInternalServerError, err) - return - } - doc.Path = h.configPath - writeConfigJSON(w, doc) - case http.MethodPost: - if status, err := validateLocalJSONRequest(r); err != nil { - writeConfigErr(w, status, err) - return - } - var doc configDoc - if err := json.NewDecoder(r.Body).Decode(&doc); err != nil { - writeConfigErr(w, http.StatusBadRequest, fmt.Errorf("bad request: %w", err)) - return - } - if err := validateConfigDoc(&doc); err != nil { - writeConfigErr(w, http.StatusBadRequest, err) - return - } - if err := writeConfigDoc(h.configPath, &doc); err != nil { - writeConfigErr(w, http.StatusInternalServerError, err) - return - } - writeConfigJSON(w, map[string]string{"status": "saved"}) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - -// handleModels fetches model IDs through the local config server so the page -// does not need cross-origin access to a provider API. -func (*configHandler) handleModels(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - if status, err := validateLocalJSONRequest(r); err != nil { - writeConfigErr(w, status, err) - return - } - - var input modelListRequest - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - writeConfigErr(w, http.StatusBadRequest, fmt.Errorf("bad request: %w", err)) - return - } - - baseURL := strings.TrimSpace(input.BaseURL) - apiKey := strings.TrimSpace(input.APIKey) - anthropic := isAnthropicAPI(input.API, baseURL, input.Model) +// fetchModelIDs asks a provider which models it advertises, so `phi config` can +// fill a model name from a list instead of a typed-in guess. It runs on a +// background goroutine owned by the config overlay. +func fetchModelIDs(ctx context.Context, baseURL, apiKey, api, model string) ([]string, error) { + baseURL = strings.TrimSpace(baseURL) + anthropic := isAnthropicAPI(api, baseURL, model) if baseURL == "" { if anthropic { baseURL = "https://api.anthropic.com" @@ -193,40 +48,35 @@ func (*configHandler) handleModels(w http.ResponseWriter, r *http.Request) { } endpoint, err := modelListEndpoint(baseURL, anthropic) if err != nil { - writeConfigErr(w, http.StatusBadRequest, err) - return + return nil, err } - ctx, cancel := context.WithTimeout(r.Context(), modelListRequestLimit) + ctx, cancel := context.WithTimeout(ctx, modelListTimeout) defer cancel() request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) if err != nil { - writeConfigErr(w, http.StatusBadRequest, fmt.Errorf("build model list request: %w", err)) - return + return nil, fmt.Errorf("build model list request: %w", err) } request.Header.Set("Accept", "application/json") if anthropic { - request.Header.Set("X-Api-Key", apiKey) + request.Header.Set("X-Api-Key", strings.TrimSpace(apiKey)) request.Header.Set("Anthropic-Version", anthropicAPIVersion) } else { - request.Header.Set("Authorization", "Bearer "+apiKey) + request.Header.Set("Authorization", "Bearer "+strings.TrimSpace(apiKey)) } response, err := modelListHTTPClient().Do(request) if err != nil { - writeConfigErr(w, http.StatusBadGateway, fmt.Errorf("fetch model list: %w", err)) - return + return nil, fmt.Errorf("fetch model list: %w", err) } defer response.Body.Close() body, err := io.ReadAll(io.LimitReader(response.Body, modelListBodyLimit+1)) if err != nil { - writeConfigErr(w, http.StatusBadGateway, fmt.Errorf("read model list: %w", err)) - return + return nil, fmt.Errorf("read model list: %w", err) } if int64(len(body)) > modelListBodyLimit { - writeConfigErr(w, http.StatusBadGateway, errors.New("model list response is too large")) - return + return nil, errors.New("model list response is too large") } if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { message := strings.TrimSpace(string(body)) @@ -236,22 +86,21 @@ func (*configHandler) handleModels(w http.ResponseWriter, r *http.Request) { if message == "" { message = response.Status } - writeConfigErr(w, http.StatusBadGateway, fmt.Errorf("model list request failed: %s", message)) - return + return nil, fmt.Errorf("model list request failed: %s", message) } var payload modelListResponse if err := json.Unmarshal(body, &payload); err != nil { - writeConfigErr(w, http.StatusBadGateway, fmt.Errorf("decode model list: %w", err)) - return + return nil, fmt.Errorf("decode model list: %w", err) } models := collectModelIDs(append(payload.Data, payload.Models...)) sort.Strings(models) - writeConfigJSON(w, struct { - Models []string `json:"models"` - }{Models: models}) + return models, nil } +// modelListHTTPClient is the shared client with one restriction on top: a +// redirect must stay on the same origin, so a provider cannot bounce the API +// key to another host. func modelListHTTPClient() *http.Client { client := *util.DefaultHTTPClient() client.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -275,11 +124,14 @@ func isAnthropicAPI(api, baseURL, model string) bool { case "OpenAI", "OpenAIResponses", "Gemini": return false } - // Empty api: keep legacy heuristics so older editor sessions still fetch. + // Empty api: keep legacy heuristics so an entry with only claude-ish values + // still probes the Anthropic endpoint. return strings.Contains(strings.ToLower(baseURL), "anthropic") || strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "claude") } +// modelListEndpoint resolves the /models URL for a base URL, tolerating a base +// that already ends in /models or /v1. func modelListEndpoint(baseURL string, anthropic bool) (string, error) { u, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil || u.Scheme == "" || u.Host == "" { @@ -325,130 +177,3 @@ func collectModelIDs(items []modelListItem) []string { } return models } - -// validateLocalJSONRequest requires browser POSTs to use a non-simple content -// type and come from the config page's own origin. ServeHTTP separately checks -// that every API request uses a loopback Host. -func validateLocalJSONRequest(r *http.Request) (int, error) { - mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil || mediaType != "application/json" { - return http.StatusUnsupportedMediaType, errors.New("content type must be application/json") - } - origin := r.Header.Get("Origin") - originURL, err := url.Parse(origin) - if err != nil || origin == "" || originURL.Scheme == "" || originURL.Host == "" || - originURL.User != nil || originURL.Path != "" || originURL.RawQuery != "" || originURL.Fragment != "" { - return http.StatusForbidden, errors.New("request origin is not allowed") - } - - expectedScheme := "http" - if r.TLS != nil { - expectedScheme = "https" - } - if !strings.EqualFold(originURL.Scheme, expectedScheme) || !strings.EqualFold(originURL.Host, r.Host) { - return http.StatusForbidden, errors.New("request origin is not allowed") - } - return 0, nil -} - -func isLoopbackHost(rawHost string) bool { - u, err := url.Parse("http://" + rawHost) - if err != nil || u.Host != rawHost || u.User != nil || u.Path != "" { - return false - } - hostname := u.Hostname() - if strings.EqualFold(hostname, "localhost") { - return true - } - ip := net.ParseIP(hostname) - return ip != nil && ip.IsLoopback() -} - -// readConfigDoc loads the current config file into the editor document. A -// missing file yields an empty document so the page can bootstrap a config. -func readConfigDoc(path string) (*configDoc, error) { - doc := &configDoc{} - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return doc, nil - } - return nil, err - } - if err := yaml.Unmarshal(data, doc); err != nil { - return nil, err - } - return doc, nil -} - -func validateConfigDoc(doc *configDoc) error { - if len(doc.Models) == 0 { - return errors.New("at least one model is required") - } - hasDefault := false - for i := range doc.Models { - m := &doc.Models[i] - if m.Name == "" { - return fmt.Errorf("model %d has no name", i+1) - } - if !m.Default { - continue - } - if hasDefault { - return errors.New("only one model may be marked default") - } - hasDefault = true - if m.APIKey == "" { - return fmt.Errorf("default model %q is missing api_key", m.Name) - } - } - if !hasDefault { - doc.Models[0].Default = true - if doc.Models[0].APIKey == "" { - return fmt.Errorf("default model %q is missing api_key", doc.Models[0].Name) - } - } - return nil -} - -// writeConfigDoc backs up the current file and writes the document as YAML. -func writeConfigDoc(path string, doc *configDoc) error { - data, err := yaml.Marshal(doc) - if err != nil { - return err - } - if cur, err := os.ReadFile(path); err == nil { - //nolint:gosec // G306: config backup stays user-readable - if err := os.WriteFile(path+".bak", cur, 0o644); err != nil { - return fmt.Errorf("backup config: %w", err) - } - } - return os.WriteFile(path, data, 0o644) //nolint:gosec // G306: config.yaml is meant to be user-readable -} - -func writeConfigJSON(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(v); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func writeConfigErr(w http.ResponseWriter, status int, err error) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) -} - -// openBrowser best-effort opens the editor URL in the default browser. -func openBrowser(ctx context.Context, pageURL string) { - var cmd *exec.Cmd - switch runtime.GOOS { - case "darwin": - cmd = exec.CommandContext(ctx, "open", pageURL) - case "linux": - cmd = exec.CommandContext(ctx, "xdg-open", pageURL) - default: - return - } - _ = cmd.Start() -} diff --git a/cmd/config.html b/cmd/config.html deleted file mode 100644 index 6242ad0e..00000000 --- a/cmd/config.html +++ /dev/null @@ -1,1866 +0,0 @@ - - - - - -phi config - - - -

- - -
-
-
-
-
-

Models

-
0 models
-
-

Keep at least one model; only one can be the startup model.

-
- -
-
-
- -
-
-
-
-
General
-

Choose where skill files are loaded from.

-
-
-
-
- -
Reads SKILL.md files inside; leave empty for the default path.
- -
-
-
- -
-
-
-
Permission mode
-

Controls how the agent runs commands, reads or writes files, and makes network requests.

-
-
-
-
- -
Different modes change which actions require confirmation.
- -

-
-
- -
How long permission prompts wait before timing out.
- -
-
-
- - -
-
-
- -
-
-
-

Tool rules

-

Further limit the agent by command and destination host.

-
-
-
-
- - -
-
-
-
- -
- -
-
- - - - diff --git a/cmd/config_cmd.go b/cmd/config_cmd.go index b46d5975..c7f593de 100644 --- a/cmd/config_cmd.go +++ b/cmd/config_cmd.go @@ -1,61 +1,63 @@ package main import ( - "context" - "errors" "fmt" - "net" - "net/http" "os" - "os/signal" - "time" + + "github.com/pulseaiclub/xui" cli "github.com/pulseaiclub/pli" + "github.com/pulseaiclub/phi/internal/components" + "github.com/pulseaiclub/phi/internal/components/app" "github.com/pulseaiclub/phi/internal/project" + "github.com/pulseaiclub/phi/internal/tui/configui" ) var configCommand = cli.Command{ Name: "config", - Desc: "open the HTML config editor (local web server)", - Long: "Open the HTML config editor (starts a local web server on 127.0.0.1).", + Desc: "edit the meta config in a terminal UI", + Long: `Edit the meta config in a full-screen terminal UI (models, skills, +permissions, sub-agents). Changes are written only on save; the previous file +is kept next to it as config.yaml.bak.`, Run: func(_ []string, _ cli.Flags) error { return runConfigEditor() }, } -// runConfigEditor starts a local web server (loopback only) that edits -// config.yaml in the browser. +// runConfigEditor opens ~/.phi/config.yaml in the config form. func runConfigEditor() error { proj := project.GetDefaultProject() + path := proj.Global().ConfigFile() - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - - var lc net.ListenConfig - ln, err := lc.Listen(ctx, "tcp", "127.0.0.1:0") + doc, err := project.ReadConfigDoc(path) if err != nil { return err } - addr := ln.Addr().(*net.TCPAddr) - pageURL := fmt.Sprintf("http://127.0.0.1:%d/", addr.Port) - fmt.Fprintf(os.Stderr, "phi config: %s\n config: %s\n Ctrl-C to stop\n", pageURL, proj.Global().ConfigFile()) - openBrowser(ctx, pageURL) - - srv := &http.Server{ - Handler: &configHandler{configPath: proj.Global().ConfigFile()}, - ReadHeaderTimeout: 10 * time.Second, - } - errc := make(chan error, 1) - go func() { errc <- srv.Serve(ln) }() - select { - case err := <-errc: - if err != nil && !errors.Is(err, http.ErrServerClosed) { - return err + vx, err := xui.New(xui.Options{Mouse: true, BracketedPaste: true}) + if err != nil { + fmt.Fprintln(os.Stderr, "phi: terminal UI:", err) + return exitCode(ExitError) + } + defer func(vx *xui.XUI) { + if err := vx.Close(); err != nil { + fmt.Fprintln(os.Stderr, "phi: close terminal:", err) } - case <-ctx.Done(): - _ = srv.Close() + }(vx) + + application := app.NewApp(vx) + ui := configui.New( + doc, + path, + proj.Global().SkillsDir(), + components.DefaultTheme(), + fetchModelIDs, + application.RequestRedraw, + ) + if err := application.Run(ui); err != nil { + fmt.Fprintln(os.Stderr, "phi:", err) + return exitCode(ExitError) } return nil } diff --git a/cmd/config_test.go b/cmd/config_test.go index b5d13874..6308e1c8 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -1,184 +1,26 @@ package main import ( - "context" "encoding/json" - "fmt" - "io" "net/http" "net/http/httptest" - "os" - "path/filepath" - "strings" "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/pulseaiclub/phi/internal/project" ) -const configUIFixture = `models: - - name: model-a - api_key: key-a - base_url: https://a.example/v1 - context_window: 1000 - image_enabled: true - default: true - - name: model-b - api_key: key-b - base_url: https://b.example/v1 -permissions: - mode: readonly - dangerously_allow_all: true - bash: - default: ask - allow: - - "^git " -agents: - enabled: false -` - -func TestConfigHandlerGETAndRoundTrip(t *testing.T) { - home := t.TempDir() - phiDir := filepath.Join(home, ".phi") - require.NoError(t, os.MkdirAll(phiDir, 0o755)) - path := filepath.Join(phiDir, "config.yaml") - require.NoError(t, os.WriteFile(path, []byte(configUIFixture), 0o644)) - - h := &configHandler{configPath: path} - - // GET serves the current document. - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newLocalAPIRequest(http.MethodGet, "/api/config", nil)) - require.Equal(t, http.StatusOK, rr.Code) - var got configDoc - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) - require.Len(t, got.Models, 2) - assert.Equal(t, "model-a", got.Models[0].Name) - assert.True(t, got.Models[0].Default) - require.NotNil(t, got.Models[0].ImageEnabled) - assert.True(t, *got.Models[0].ImageEnabled) - require.NotNil(t, got.Permissions) - require.NotNil(t, got.Permissions.Bash) - assert.Equal(t, []string{"^git "}, got.Permissions.Bash.Allow) - require.NotNil(t, got.Agents) - require.NotNil(t, got.Agents.Enabled) - assert.False(t, *got.Agents.Enabled) - assert.Equal(t, path, got.Path) - - // Edit: drop model-b and change the api_key, keep permissions untouched. - got.Models = got.Models[:1] - got.Models[0].APIKey = "new-key" - body, err := json.Marshal(got) - require.NoError(t, err) - - rr = httptest.NewRecorder() - h.ServeHTTP(rr, newJSONAPIRequest("/api/config", strings.NewReader(string(body)))) - require.Equal(t, http.StatusOK, rr.Code) - - // The app must be able to load the written file with the same results. - // os.UserHomeDir uses HOME on Unix and USERPROFILE on Windows. - t.Setenv("HOME", home) - t.Setenv("USERPROFILE", home) - p, err := project.Discover("") - require.NoError(t, err) - require.NoError(t, p.LoadConfig()) - cfg := p.Config() - require.Len(t, cfg.Models, 1) - assert.Equal(t, "model-a", cfg.Model().Name) - assert.Equal(t, "new-key", cfg.Model().APIKey) - assert.True(t, cfg.Model().ImageEnabled) - assert.Equal(t, "readonly", string(cfg.Permissions.Mode)) - assert.True(t, cfg.Permissions.DangerouslyAllowAll) - assert.Equal(t, []string{"^git "}, cfg.Permissions.BashAllow) - assert.False(t, cfg.Agents.Enabled) - - // The previous file content is kept as a backup. - bak, err := os.ReadFile(path + ".bak") - require.NoError(t, err) - assert.Contains(t, string(bak), "model-b") -} - -func TestConfigHandlerMissingFile(t *testing.T) { - h := &configHandler{configPath: filepath.Join(t.TempDir(), "nope.yaml")} - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newLocalAPIRequest(http.MethodGet, "/api/config", nil)) - require.Equal(t, http.StatusOK, rr.Code) - var doc configDoc - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &doc)) - assert.Empty(t, doc.Models) -} - -func TestConfigHandlerValidation(t *testing.T) { - h := &configHandler{configPath: filepath.Join(t.TempDir(), "config.yaml")} - +func TestFetchModelIDs(t *testing.T) { cases := []struct { - name string - doc configDoc - }{ - {"no models", configDoc{}}, - {"default missing api_key", configDoc{Models: []modelDoc{{Name: "m"}}}}, - { - "two defaults", - configDoc{ - Models: []modelDoc{{Name: "a", APIKey: "k", Default: true}, {Name: "b", APIKey: "k", Default: true}}, - }, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - body, err := json.Marshal(tc.doc) - require.NoError(t, err) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newJSONAPIRequest("/api/config", strings.NewReader(string(body)))) - require.Equal(t, http.StatusBadRequest, rr.Code) - }) - } - - // A minimal valid document saves and marks the first model default. - doc := configDoc{Models: []modelDoc{{Name: "m", APIKey: "k"}}} - body, err := json.Marshal(doc) - require.NoError(t, err) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newJSONAPIRequest("/api/config", strings.NewReader(string(body)))) - require.Equal(t, http.StatusOK, rr.Code) - - data, err := os.ReadFile(h.configPath) - require.NoError(t, err) - require.Contains(t, string(data), "default: true") - require.Contains(t, string(data), "name: m") -} - -func TestConfigHandlerServesPage(t *testing.T) { - h := &configHandler{configPath: filepath.Join(t.TempDir(), "config.yaml")} - rr := httptest.NewRecorder() - h.ServeHTTP(rr, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)) - require.Equal(t, http.StatusOK, rr.Code) - body := rr.Body.String() - assert.Contains(t, body, `id="langToggle"`) - assert.Contains(t, body, "phi-config-lang") - assert.Contains(t, body, "配置中心") - assert.Contains(t, body, "Config") - require.Contains(t, body, `type: "password"`) - assert.Contains(t, body, "tokens") - assert.Contains(t, body, "seconds") - assert.Contains(t, body, "apiRouter") - assert.Contains(t, body, "thinkLevel") - assert.Contains(t, body, "/api/config") - assert.Contains(t, body, "/api/models") -} - -func TestConfigHandlerListsModels(t *testing.T) { - cases := []struct { - name string - api string - model string - response string - wantPath string - wantModels []string - checkAuth func(*testing.T, *http.Request) + name string + api string + model string + response string + wantPath string + wantModels []string + checkAuth func(*testing.T, *http.Request) + withBaseURL bool }{ { name: "openai compatible", @@ -191,6 +33,7 @@ func TestConfigHandlerListsModels(t *testing.T) { assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization")) assert.Empty(t, r.Header.Get("Anthropic-Version")) }, + withBaseURL: true, }, { name: "anthropic explicit api", @@ -201,26 +44,30 @@ func TestConfigHandlerListsModels(t *testing.T) { wantModels: []string{"claude-sonnet-4-20250514"}, checkAuth: func(t *testing.T, r *http.Request) { assert.Equal(t, "test-key", r.Header.Get("X-Api-Key")) - assert.Equal(t, "2023-06-01", r.Header.Get("Anthropic-Version")) + assert.Equal(t, anthropicAPIVersion, r.Header.Get("Anthropic-Version")) }, + withBaseURL: true, }, { name: "claude name with openai api stays openai", api: "OpenAI", model: "claude-via-proxy", - response: `{"data":[{"id":"proxy-model"}]}`, + response: `{"models":[{"name":"proxy-model"}]}`, wantPath: "/v1/models", wantModels: []string{"proxy-model"}, checkAuth: func(t *testing.T, r *http.Request) { assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization")) assert.Empty(t, r.Header.Get("X-Api-Key")) }, + withBaseURL: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + var hits atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) assert.Equal(t, tc.wantPath, r.URL.Path) tc.checkAuth(t, r) w.Header().Set("Content-Type", "application/json") @@ -228,53 +75,79 @@ func TestConfigHandlerListsModels(t *testing.T) { })) defer server.Close() - body, err := json.Marshal(modelListRequest{ - BaseURL: server.URL + "/v1", - APIKey: "test-key", - Model: tc.model, - API: tc.api, - }) + baseURL := "" + if tc.withBaseURL { + baseURL = server.URL + "/v1" + } + got, err := fetchModelIDs(t.Context(), baseURL, "test-key", tc.api, tc.model) require.NoError(t, err) + assert.Equal(t, tc.wantModels, got) + assert.Equal(t, int32(1), hits.Load()) + }) + } +} - h := &configHandler{configPath: filepath.Join(t.TempDir(), "config.yaml")} - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newJSONAPIRequest("/api/models", strings.NewReader(string(body)))) - require.Equal(t, http.StatusOK, rr.Code) +func TestFetchModelIDsDefaultsToOpenAI(t *testing.T) { + // No base URL and no claude-ish hints: the request must go to the public + // OpenAI endpoint. The DNS lookup fails here, which is fine — we only care + // that the failure is a transport error and not an arg error. + _, err := fetchModelIDs(t.Context(), "", "test-key", "", "gpt-4o") + require.Error(t, err) + assert.NotErrorIs(t, err, http.ErrNotSupported) +} - var got struct { - Models []string `json:"models"` - } - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) - assert.Equal(t, tc.wantModels, got.Models) +func TestFetchModelIDsRejectsBadBaseURL(t *testing.T) { + cases := []struct{ name, baseURL string }{ + {"relative", "api.example.com/v1"}, + {"no host", "https:///v1"}, + {"bad scheme", "ftp://api.example.com/v1"}, + {"not a url", "://nope"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := fetchModelIDs(t.Context(), tc.baseURL, "key", "OpenAI", "m") + require.Error(t, err) }) } } -func TestConfigHandlerModelListRedirects(t *testing.T) { +func TestFetchModelIDsSurfacesProviderError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"bad key"}`)) + })) + defer server.Close() + + _, err := fetchModelIDs(t.Context(), server.URL, "test-key", "OpenAI", "m") + require.Error(t, err) + assert.Contains(t, err.Error(), "bad key") +} + +func TestFetchModelIDsRedirects(t *testing.T) { t.Run("same origin is followed", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/models": http.Redirect(w, r, "/models-final", http.StatusTemporaryRedirect) case "/models-final": - assert.Equal(t, "test-key", r.Header.Get("X-Api-Key")) - _, _ = w.Write([]byte(`{"data":[{"id":"claude-model"}]}`)) + assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization")) + _, _ = w.Write([]byte(`{"data":[{"id":"via-redirect"}]}`)) default: http.NotFound(w, r) } })) defer server.Close() - rr := requestModelList(t, server.URL+"/v1", "claude-model") - require.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "claude-model") + got, err := fetchModelIDs(t.Context(), server.URL+"/v1", "test-key", "OpenAI", "m") + require.NoError(t, err) + assert.Equal(t, []string{"via-redirect"}, got) }) t.Run("cross origin is rejected without forwarding key", func(t *testing.T) { var targetRequests atomic.Int32 target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { targetRequests.Add(1) - assert.Empty(t, r.Header.Get("X-Api-Key")) + assert.Empty(t, r.Header.Get("Authorization")) _, _ = w.Write([]byte(`{"data":[]}`)) })) defer target.Close() @@ -284,173 +157,67 @@ func TestConfigHandlerModelListRedirects(t *testing.T) { })) defer source.Close() - rr := requestModelList(t, source.URL+"/v1", "claude-model") - require.Equal(t, http.StatusBadGateway, rr.Code) + _, err := fetchModelIDs(t.Context(), source.URL+"/v1", "test-key", "OpenAI", "m") + require.Error(t, err) assert.Zero(t, targetRequests.Load()) }) } -func TestConfigHandlerRejectsUnsafePOSTs(t *testing.T) { - const localHost = "127.0.0.1:43210" - const localOrigin = "http://127.0.0.1:43210" - - var targetRequests atomic.Int32 - target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - targetRequests.Add(1) - _, _ = w.Write([]byte(`{"data":[]}`)) - })) - defer target.Close() - - configBody := `{"models":[{"name":"model","apiKey":"key","default":true}]}` - modelBody := fmt.Sprintf(`{"baseUrl":%q,"apiKey":"key","model":"model"}`, target.URL) +func TestModelListEndpoint(t *testing.T) { cases := []struct { - name string - path string - body string - contentType string - host string - origin string - wantStatus int + name string + baseURL string + anthropic bool + want string }{ - { - "config rejects non-JSON", - "/api/config", - configBody, - "text/plain", - localHost, - localOrigin, - http.StatusUnsupportedMediaType, - }, - { - "config rejects missing origin", - "/api/config", - configBody, - "application/json", - localHost, - "", - http.StatusForbidden, - }, - { - "config rejects cross-origin", - "/api/config", - configBody, - "application/json", - localHost, - "https://attacker.example", - http.StatusForbidden, - }, - { - "config rejects non-loopback host", - "/api/config", - configBody, - "application/json", - "attacker.example", - "http://attacker.example", - http.StatusForbidden, - }, - { - "models rejects non-JSON", - "/api/models", - modelBody, - "text/plain", - localHost, - localOrigin, - http.StatusUnsupportedMediaType, - }, - { - "models rejects missing origin", - "/api/models", - modelBody, - "application/json", - localHost, - "", - http.StatusForbidden, - }, - { - "models rejects cross-origin", - "/api/models", - modelBody, - "application/json", - localHost, - "https://attacker.example", - http.StatusForbidden, - }, - { - "models rejects non-loopback host", - "/api/models", - modelBody, - "application/json", - "attacker.example", - "http://attacker.example", - http.StatusForbidden, - }, + {"openai base", "https://api.openai.com/v1", false, "https://api.openai.com/v1/models"}, + {"bare host", "https://example.com", false, "https://example.com/models"}, + {"already models", "https://example.com/v1/models", false, "https://example.com/v1/models"}, + {"anthropic adds v1", "https://api.anthropic.com", true, "https://api.anthropic.com/v1/models"}, + {"drops query", "https://example.com/v1?x=1", false, "https://example.com/v1/models"}, + {"trailing slash", "https://example.com/v1/", false, "https://example.com/v1/models"}, } - for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - before := targetRequests.Load() - h := &configHandler{configPath: filepath.Join(t.TempDir(), "config.yaml")} - req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, tc.path, strings.NewReader(tc.body)) - req.Host = tc.host - req.Header.Set("Content-Type", tc.contentType) - if tc.origin != "" { - req.Header.Set("Origin", tc.origin) - } - rr := httptest.NewRecorder() - - h.ServeHTTP(rr, req) - - assert.Equal(t, tc.wantStatus, rr.Code) - assert.Equal(t, before, targetRequests.Load()) - _, err := os.Stat(h.configPath) - assert.ErrorIs(t, err, os.ErrNotExist) + got, err := modelListEndpoint(tc.baseURL, tc.anthropic) + require.NoError(t, err) + assert.Equal(t, tc.want, got) }) } } -func TestConfigHandlerRejectsNonLoopbackGET(t *testing.T) { - path := filepath.Join(t.TempDir(), "config.yaml") - require.NoError(t, os.WriteFile(path, []byte(`models: - - name: secret-model - api_key: secret-key - default: true -`), 0o600)) - - h := &configHandler{configPath: path} - req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/config", http.NoBody) - req.Host = "attacker.example" - rr := httptest.NewRecorder() - - h.ServeHTTP(rr, req) - - require.Equal(t, http.StatusForbidden, rr.Code) - assert.NotContains(t, rr.Body.String(), "secret-key") +func TestIsAnthropicAPI(t *testing.T) { + cases := []struct { + api, baseURL, model string + want bool + }{ + {"Anthropic", "", "", true}, + {"OpenAI", "https://anthropic.example", "claude-3", false}, + {"Gemini", "https://x.example", "gemini-pro", false}, + {"", "https://api.anthropic.com", "m", true}, + {"", "https://x.example", "claude-sonnet", true}, + {"", "https://x.example", "m", false}, + } + for _, tc := range cases { + assert.Equal(t, tc.want, isAnthropicAPI(tc.api, tc.baseURL, tc.model), tc) + } } -func requestModelList(t *testing.T, baseURL, model string) *httptest.ResponseRecorder { - t.Helper() - body, err := json.Marshal(modelListRequest{ - BaseURL: baseURL, - APIKey: "test-key", - Model: model, +func TestCollectModelIDsPrefersID(t *testing.T) { + got := collectModelIDs([]modelListItem{ + {ID: "id", Name: "name", DisplayName: "display"}, + {Name: "name-only"}, + {DisplayName: "display-only"}, + {}, + {ID: "id"}, }) - require.NoError(t, err) - - h := &configHandler{configPath: filepath.Join(t.TempDir(), "config.yaml")} - rr := httptest.NewRecorder() - h.ServeHTTP(rr, newJSONAPIRequest("/api/models", strings.NewReader(string(body)))) - return rr -} - -func newJSONAPIRequest(target string, body io.Reader) *http.Request { - req := newLocalAPIRequest(http.MethodPost, target, body) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Origin", "http://"+req.Host) - return req + assert.Equal(t, []string{"id", "name-only", "display-only"}, got) } -func newLocalAPIRequest(method, target string, body io.Reader) *http.Request { - req := httptest.NewRequestWithContext(context.Background(), method, target, body) - req.Host = "127.0.0.1:43210" - return req +// modelListResponse must decode both the OpenAI and Anthropic envelopes. +func TestModelListResponseDecodesBothEnvelopes(t *testing.T) { + var payload modelListResponse + require.NoError(t, json.Unmarshal([]byte(`{"data":[{"id":"a"}],"models":[{"id":"b"}]}`), &payload)) + assert.Len(t, payload.Data, 1) + assert.Len(t, payload.Models, 1) } diff --git a/cmd/main.go b/cmd/main.go index b90f99c2..832bc73c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,7 @@ func buildRoot() *cli.Command { Desc: "terminal coding agent", Long: `phi start the interactive TUI phi tui start the interactive TUI -phi config open the HTML config editor (local web server) +phi config edit the meta config in a terminal UI phi update install the latest release (see 'phi update --help') phi run -p "..." run one agent loop headlessly (see 'phi run --help') phi sessions list list persisted sessions for this directory diff --git a/internal/project/config.go b/internal/project/config.go index c83925bc..6793ae08 100644 --- a/internal/project/config.go +++ b/internal/project/config.go @@ -1,7 +1,6 @@ package project import ( - "errors" "fmt" "os" "strings" @@ -36,9 +35,9 @@ type AgentsConfig struct { // AgentsRoleModels maps sub-agent roles to configured model names. type AgentsRoleModels struct { - Explore string `yaml:"explore"` - Review string `yaml:"review"` - Worker string `yaml:"worker"` + Explore string `yaml:"explore,omitempty"` + Review string `yaml:"review,omitempty"` + Worker string `yaml:"worker,omitempty"` } // Model returns the default model config with the skill path applied, ready @@ -136,13 +135,13 @@ func parseConfigFile(path string) (*Config, error) { // Pointer fields distinguish "key absent" from "zero value", so per-key // defaults (and permission.DefaultPolicy) survive decoding and are only // overridden by keys that are actually present. - var raw fileConfig + var raw ConfigDoc if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } for _, m := range raw.Models { - mc := modelEntryToConfig(m) + mc := modelDocToConfig(m) if m.Default && cfg.DefaultModel == "" { cfg.DefaultModel = mc.Name } @@ -169,7 +168,7 @@ func parseConfigFile(path string) (*Config, error) { return cfg, nil } -func modelEntryToConfig(m modelEntry) llm.ModelConfig { +func modelDocToConfig(m ModelDoc) llm.ModelConfig { cfg := llm.ModelConfig{Name: m.Name, APIKey: m.APIKey, BaseURL: m.BaseURL} // A built-in preset supplies base_url / context_window / image_enabled / api // when the entry omits them; the explicit fields below still win so @@ -191,7 +190,7 @@ func modelEntryToConfig(m modelEntry) llm.ModelConfig { cfg.Think = pc.Think } if m.API != "" { - cfg.API = m.API + cfg.API = llm.RouterType(m.API) } if m.ContextWindow != nil && *m.ContextWindow > 0 { cfg.ContextWindow = *m.ContextWindow @@ -202,8 +201,8 @@ func modelEntryToConfig(m modelEntry) llm.ModelConfig { if m.ThinkEnabled != nil { cfg.Think.Enabled = *m.ThinkEnabled } - if m.ThinkLevel != nil && *m.ThinkLevel != "" { - mode := llm.ThinkMode(*m.ThinkLevel) + if m.ThinkLevel != "" { + mode := llm.ThinkMode(m.ThinkLevel) cfg.Think.Mode = mode cfg.Think.Enabled = mode != llm.Off } @@ -213,51 +212,11 @@ func modelEntryToConfig(m modelEntry) llm.ModelConfig { return cfg } -// fileConfig mirrors the YAML keys in ~/.phi/config.yaml. -type fileConfig struct { - Models []modelEntry `yaml:"models"` - SkillPath *string `yaml:"skill_path"` - Permissions *permConfig `yaml:"permissions"` - Agents *agentsConfig `yaml:"agents"` -} - -type agentsConfig struct { - // Enabled is a pointer so omitting the key keeps the default (on). - Enabled *bool `yaml:"enabled"` - Models *AgentsRoleModels `yaml:"models"` -} - -type modelEntry struct { - Name string `yaml:"name"` - APIKey string `yaml:"api_key"` - BaseURL string `yaml:"base_url"` - ContextWindow *int `yaml:"context_window"` - ImageEnabled *bool `yaml:"image_enabled"` - API llm.RouterType `yaml:"api"` - Default bool `yaml:"default"` - ThinkEnabled *bool `yaml:"think_enabled"` - ThinkLevel *string `yaml:"think_level"` -} - -type permConfig struct { - Mode permission.Mode `yaml:"mode"` - WorkspaceOnlyWrites *bool `yaml:"workspace_only_writes"` - AskTimeoutSec *int `yaml:"ask_timeout_sec"` - DangerouslyAllowAll *bool `yaml:"dangerously_allow_all"` - Bash *bashConfig `yaml:"bash"` -} - -type bashConfig struct { - Default *string `yaml:"default"` - Allow *stringList `yaml:"allow"` - Deny *stringList `yaml:"deny"` -} - // applyPermissions merges the file's permissions block over DefaultPolicy. // An explicitly set list (even an empty one) replaces the default list. -func applyPermissions(p *permission.Policy, raw *permConfig) { +func applyPermissions(p *permission.Policy, raw *PermDoc) { if raw.Mode != "" { - p.Mode = raw.Mode + p.Mode = permission.Mode(raw.Mode) } if raw.WorkspaceOnlyWrites != nil { p.WorkspaceOnlyWrites = *raw.WorkspaceOnlyWrites @@ -269,8 +228,8 @@ func applyPermissions(p *permission.Policy, raw *permConfig) { p.DangerouslyAllowAll = *raw.DangerouslyAllowAll } if b := raw.Bash; b != nil { - if b.Default != nil { - p.BashDefault = parseDecision(*b.Default, p.BashDefault) + if b.Default != "" { + p.BashDefault = parseDecision(b.Default, p.BashDefault) } if b.Allow != nil { p.BashAllow = *b.Allow @@ -281,26 +240,6 @@ func applyPermissions(p *permission.Policy, raw *permConfig) { } } -// stringList accepts either a single YAML scalar or a sequence, so both -// `allow: "go test ./..."` and the block list form in the README work. -type stringList []string - -func (s *stringList) UnmarshalYAML(node *yaml.Node) error { - switch node.Kind { - case yaml.ScalarNode: - *s = stringList{node.Value} - case yaml.SequenceNode: - items := make(stringList, 0, len(node.Content)) - for _, n := range node.Content { - items = append(items, n.Value) - } - *s = items - default: - return errors.New("expected a string or a list of strings") - } - return nil -} - func countIndent(line string) int { n := 0 for _, r := range line { diff --git a/internal/project/configdoc.go b/internal/project/configdoc.go new file mode 100644 index 00000000..9edcb3c8 --- /dev/null +++ b/internal/project/configdoc.go @@ -0,0 +1,156 @@ +package project + +import ( + "errors" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +// ConfigDoc is the editable view of ~/.phi/config.yaml: the keys +// parseConfigFile reads, with pointer fields so a key the editor never touched +// stays absent when the file is written back. `phi config` edits this document; +// the runtime keeps loading through parseConfigFile. +type ConfigDoc struct { + Models []ModelDoc `yaml:"models"` + SkillPath *string `yaml:"skill_path,omitempty"` + Permissions *PermDoc `yaml:"permissions,omitempty"` + Agents *AgentsDoc `yaml:"agents,omitempty"` +} + +// ModelDoc is one models[] entry. +type ModelDoc struct { + Name string `yaml:"name"` + // APIKey and BaseURL are omitted when empty: the loader treats an absent + // key exactly like an empty one, and a saved file reads cleaner without + // `base_url: ""` noise on every preset-backed model. + APIKey string `yaml:"api_key,omitempty"` + BaseURL string `yaml:"base_url,omitempty"` + ContextWindow *int `yaml:"context_window,omitempty"` + // ImageEnabled is a pointer so json/yaml round-trips keep "key absent" + // distinct from an explicit false. + ImageEnabled *bool `yaml:"image_enabled,omitempty"` + API string `yaml:"api,omitempty"` // OpenAI | OpenAIResponses | Anthropic | Gemini + ThinkEnabled *bool `yaml:"think_enabled,omitempty"` + ThinkLevel string `yaml:"think_level,omitempty"` + Default bool `yaml:"default,omitempty"` +} + +// PermDoc is the permissions block. +type PermDoc struct { + Mode string `yaml:"mode,omitempty"` + WorkspaceOnlyWrites *bool `yaml:"workspace_only_writes,omitempty"` + AskTimeoutSec *int `yaml:"ask_timeout_sec,omitempty"` + DangerouslyAllowAll *bool `yaml:"dangerously_allow_all,omitempty"` + Bash *BashDoc `yaml:"bash,omitempty"` +} + +// BashDoc is the permissions.bash block. +type BashDoc struct { + Default string `yaml:"default,omitempty"` + Allow *StringList `yaml:"allow,omitempty"` + Deny *StringList `yaml:"deny,omitempty"` +} + +// AgentsDoc is the agents block. +type AgentsDoc struct { + // Enabled is a pointer so omitting the key keeps the default (on) when only + // agents.models is set. + Enabled *bool `yaml:"enabled,omitempty"` + Models *AgentsRoleModels `yaml:"models,omitempty"` +} + +// StringList accepts either a single YAML scalar or a sequence, so both +// `allow: "go test ./..."` and the block list form in the README work. +type StringList []string + +func (s *StringList) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + case yaml.ScalarNode: + *s = StringList{node.Value} + case yaml.SequenceNode: + items := make(StringList, 0, len(node.Content)) + for _, n := range node.Content { + items = append(items, n.Value) + } + *s = items + default: + return errors.New("expected a string or a list of strings") + } + return nil +} + +// ReadConfigDoc loads path into an editable document. A missing file yields an +// empty document so the editor can bootstrap a config. +func ReadConfigDoc(path string) (*ConfigDoc, error) { + doc := &ConfigDoc{} + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return doc, nil + } + return nil, fmt.Errorf("read config %s: %w", path, err) + } + if err := yaml.Unmarshal(data, doc); err != nil { + return nil, fmt.Errorf("parse %s: %w", path, err) + } + return doc, nil +} + +// Validate reports whether doc is safe to write. It does not mutate doc: a +// document with no explicit default is valid, since the loader falls back to +// the first model. +func (d *ConfigDoc) Validate() error { + if len(d.Models) == 0 { + return errors.New("at least one model is required") + } + seen := make(map[string]struct{}, len(d.Models)) + defaults := 0 + for i, m := range d.Models { + name := strings.TrimSpace(m.Name) + if name == "" { + return fmt.Errorf("model %d has no name", i+1) + } + if _, dup := seen[name]; dup { + return fmt.Errorf("duplicate model name %q", name) + } + seen[name] = struct{}{} + if m.Default { + defaults++ + if strings.TrimSpace(m.APIKey) == "" { + return fmt.Errorf("default model %q is missing api_key", name) + } + } + } + if defaults > 1 { + return errors.New("only one model may be marked default") + } + // With no explicit default the loader starts with the first entry, so that + // entry is the one that has to be usable. + if defaults == 0 && strings.TrimSpace(d.Models[0].APIKey) == "" { + return fmt.Errorf("model %q is used by default and is missing api_key", strings.TrimSpace(d.Models[0].Name)) + } + return nil +} + +// Save writes doc to path as YAML, keeping the previous file as path+".bak" so +// a bad edit never costs the user their only copy. +func (d *ConfigDoc) Save(path string) error { + data, err := yaml.Marshal(d) + if err != nil { + return fmt.Errorf("encode config: %w", err) + } + if cur, err := os.ReadFile(path); err == nil { + //nolint:gosec // G306: config backup stays user-readable + if err := os.WriteFile(path+".bak", cur, 0o644); err != nil { + return fmt.Errorf("backup config: %w", err) + } + } + //nolint:gosec // G306: config.yaml is meant to be user-readable + if err := os.WriteFile(path, data, 0o644); err != nil { + return fmt.Errorf("write config %s: %w", path, err) + } + return nil +} diff --git a/internal/project/configdoc_test.go b/internal/project/configdoc_test.go new file mode 100644 index 00000000..65bd5914 --- /dev/null +++ b/internal/project/configdoc_test.go @@ -0,0 +1,152 @@ +package project + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const configDocFixture = `models: + - name: model-a + api_key: key-a + base_url: https://a.example/v1 + context_window: 1000 + image_enabled: true + default: true + - name: model-b + api_key: key-b + base_url: https://b.example/v1 +permissions: + mode: readonly + dangerously_allow_all: true + bash: + default: ask + allow: + - "^git " +agents: + enabled: false +` + +func TestConfigDocRoundTripThroughLoader(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(configDocFixture), 0o600)) + + doc, err := ReadConfigDoc(path) + require.NoError(t, err) + require.Len(t, doc.Models, 2) + assert.Equal(t, "model-a", doc.Models[0].Name) + assert.True(t, doc.Models[0].Default) + require.NotNil(t, doc.Models[0].ImageEnabled) + assert.True(t, *doc.Models[0].ImageEnabled) + require.NotNil(t, doc.Permissions) + require.NotNil(t, doc.Permissions.Bash) + require.NotNil(t, doc.Permissions.Bash.Allow) + assert.Equal(t, StringList{"^git "}, *doc.Permissions.Bash.Allow) + require.NotNil(t, doc.Agents) + require.NotNil(t, doc.Agents.Enabled) + assert.False(t, *doc.Agents.Enabled) + + // An edit then a save must load back with the same values the runtime uses. + doc.Models[0].APIKey = "new-key" + doc.Models = doc.Models[:1] + require.NoError(t, doc.Save(path)) + + loaded, err := parseConfigFile(path) + require.NoError(t, err) + require.Len(t, loaded.Models, 1) + assert.Equal(t, "model-a", loaded.Model().Name) + assert.Equal(t, "new-key", loaded.Model().APIKey) + assert.Equal(t, 1000, loaded.Model().ContextWindow) + assert.True(t, loaded.Model().ImageEnabled) + assert.Equal(t, "readonly", string(loaded.Permissions.Mode)) + assert.True(t, loaded.Permissions.DangerouslyAllowAll) + assert.Equal(t, []string{"^git "}, loaded.Permissions.BashAllow) + assert.False(t, loaded.Agents.Enabled) + + // The previous file is kept as a backup. + backup, err := os.ReadFile(path + ".bak") + require.NoError(t, err) + assert.Contains(t, string(backup), "model-b") +} + +func TestConfigDocKeepsAbsentKeysAbsent(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte("models:\n - name: m\n api_key: k\n"), 0o600)) + + doc, err := ReadConfigDoc(path) + require.NoError(t, err) + require.NoError(t, doc.Save(path)) + + data, err := os.ReadFile(path) + require.NoError(t, err) + // Keys the editor never touched must not appear: their absence is what keeps + // the loader defaults (permissions policy, agents on) in force. + for _, key := range []string{"skill_path", "permissions", "agents", "image_enabled", "context_window"} { + assert.NotContains(t, string(data), key) + } + assert.Contains(t, string(data), "name: m") +} + +func TestConfigDocMissingFileIsEmpty(t *testing.T) { + doc, err := ReadConfigDoc(filepath.Join(t.TempDir(), "nope.yaml")) + require.NoError(t, err) + assert.Empty(t, doc.Models) + assert.Nil(t, doc.Permissions) +} + +func TestConfigDocMalformedFileFails(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte("models: [\n"), 0o600)) + _, err := ReadConfigDoc(path) + require.Error(t, err) +} + +func TestConfigDocValidate(t *testing.T) { + cases := []struct { + name string + doc ConfigDoc + wantErr string + }{ + {"no models", ConfigDoc{}, "at least one model"}, + {"unnamed model", ConfigDoc{Models: []ModelDoc{{}}}, "has no name"}, + {"duplicate names", ConfigDoc{Models: []ModelDoc{{Name: "m", APIKey: "k"}, {Name: "m"}}}, "duplicate"}, + { + "two defaults", + ConfigDoc{Models: []ModelDoc{ + {Name: "a", APIKey: "k", Default: true}, + {Name: "b", APIKey: "k", Default: true}, + }}, + "only one model", + }, + { + "default without key", + ConfigDoc{Models: []ModelDoc{{Name: "a", Default: true}}}, + "missing api_key", + }, + {"no explicit default is fine", ConfigDoc{Models: []ModelDoc{{Name: "a", APIKey: "k"}}}, ""}, + { + "first model is the implicit default", + ConfigDoc{Models: []ModelDoc{{Name: "a"}, {Name: "b", APIKey: "k"}}}, + "used by default", + }, + { + "keyless non-default model is fine", + ConfigDoc{Models: []ModelDoc{{Name: "a", APIKey: "k"}, {Name: "b"}}}, + "", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.doc.Validate() + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/internal/tui/configui/doc.go b/internal/tui/configui/doc.go new file mode 100644 index 00000000..5eb5db6a --- /dev/null +++ b/internal/tui/configui/doc.go @@ -0,0 +1,14 @@ +// Package configui is the full-screen editor behind `phi config`: one keyboard +// form over ~/.phi/config.yaml. +// +// The document (project.ConfigDoc) is the single source of truth. Rows are +// rebuilt from it after every edit, the cursor is anchored to a row key instead +// of an index, and nothing is written to disk until the user saves. +package configui + +import "context" + +// ModelLister fetches the model IDs a provider advertises for one connection. +// It runs off the UI goroutine, so implementations must be safe for concurrent +// use and must honor ctx. +type ModelLister func(ctx context.Context, baseURL, apiKey, api, model string) ([]string, error) diff --git a/internal/tui/configui/editor.go b/internal/tui/configui/editor.go new file mode 100644 index 00000000..a2ba14fc --- /dev/null +++ b/internal/tui/configui/editor.go @@ -0,0 +1,822 @@ +package configui + +import ( + "context" + "strings" + "sync" + "time" + "unicode" + + "github.com/pulseaiclub/xui" + + "github.com/pulseaiclub/phi/internal/components" + "github.com/pulseaiclub/phi/internal/components/listpicker" + "github.com/pulseaiclub/phi/internal/project" +) + +const ( + // fetchTimeout caps one provider model-list request. + fetchTimeout = 15 * time.Second + // pickerPrimaryWidth keeps choice labels in one column. + pickerPrimaryWidth = 22 +) + +// ConfigEditor is the full-screen config form. It implements components.Widget and is +// driven by the App loop, so all state lives on the UI goroutine — except the +// background model-list fetch, which hands its result back under fetchMu. +type ConfigEditor struct { + doc *project.ConfigDoc + path string + skills string + theme components.Theme + list ModelLister + redraw func() + + rows []row + cursor int + scroll int + expanded string + dirty bool + + status string + statusKind statusKind + + edit *editState + confirm *confirmState + picker listpicker.Picker + + // Painted geometry, recorded by Draw for hit testing, scrolling and the + // text caret. + width int + rowsTop int + page int + caretX int + caretY int + + fetchMu sync.Mutex + fetch fetchState +} + +// New builds the editor for doc. path is the file to write, skills is the +// directory an empty skill_path falls back to, list fetches provider model IDs +// (nil hides the feature), and redraw wakes the app after a background fetch. +func New( + doc *project.ConfigDoc, + path, skills string, + theme components.Theme, + list ModelLister, + redraw func(), +) *ConfigEditor { + e := &ConfigEditor{ + doc: doc, + path: path, + skills: skills, + theme: theme, + list: list, + redraw: redraw, + } + // Build once so the editor answers keys before the first frame. + e.rebuild() + e.move(1) + return e +} + +// Dirty reports whether the document has unsaved edits. +func (e *ConfigEditor) Dirty() bool { return e != nil && e.dirty } + +type editState struct { + key string + buf []rune + cur int +} + +// confirmState is a yes/no modal over the form. +type confirmState struct { + title string + body string + danger bool + yes bool + run func(ctx *components.EventContext) +} + +type fetchState struct { + active bool + done bool + index int + // name is the model's name when the fetch started; if the entry at index + // no longer matches it, the result is for a model that moved or was + // replaced while the request was in flight. + name string + ids []string + err error +} + +// Handle routes one event. Modal states own the keyboard outright; otherwise +// navigation, editing, and the shortcut keys share it. +func (e *ConfigEditor) Handle(ctx *components.EventContext, ev xui.Event) { + switch ev := ev.(type) { + case xui.KeyEvent: + if !ev.Press { + return + } + e.handleKey(ctx, ev) + case xui.PasteEvent: + if e.edit != nil { + e.insert(strings.NewReplacer("\n", " ", "\r", " ").Replace(ev.Text)) + ctx.ConsumeAndRedraw() + return + } + ctx.Consume = true + case xui.MouseEvent: + e.handleMouse(ctx, ev) + } +} + +func (e *ConfigEditor) handleKey(ctx *components.EventContext, k xui.KeyEvent) { + switch { + case e.picker.Open: + e.picker.Handle(ctx, k) + case e.confirm != nil: + e.handleConfirmKey(ctx, k) + case e.edit != nil: + e.handleEditKey(ctx, k) + default: + e.handleNavKey(ctx, k) + } +} + +func (e *ConfigEditor) handleNavKey(ctx *components.EventContext, k xui.KeyEvent) { + switch k.Code { + case xui.KeyUp: + e.move(-1) + case xui.KeyDown: + e.move(1) + case xui.KeyPageUp: + e.move(-max(e.page, 1)) + case xui.KeyPageDown: + e.move(max(e.page, 1)) + case xui.KeyHome: + e.jumpEdge(false) + case xui.KeyEnd: + e.jumpEdge(true) + case xui.KeyLeft: + e.cycleFocused(-1) + case xui.KeyRight: + e.cycleFocused(1) + case xui.KeyEnter, xui.KeyTab: + e.activate() + case xui.KeyEscape: + if e.expanded != "" { + e.collapse() + } else { + e.requestQuit(ctx) + } + case xui.KeyRune: + if k.Mods.Has(xui.ModAlt) { + ctx.Consume = true + return + } + if k.Mods.Has(xui.ModCtrl) { + if k.Rune != 's' && k.Rune != 'S' { + ctx.Consume = true + return + } + e.save() + break + } + switch k.Rune { + case 'k': + e.move(-1) + case 'j': + e.move(1) + case 'g': + e.jumpEdge(false) + case 'G': + e.jumpEdge(true) + case 'a': + e.addModel() + case 'd': + e.deleteFocused() + case 'f': + e.fetchModels() + case 's': + e.save() + case 'q': + e.requestQuit(ctx) + case ' ': + e.toggleFocused() + default: + ctx.Consume = true + return + } + default: + ctx.Consume = true + return + } + ctx.ConsumeAndRedraw() +} + +func (e *ConfigEditor) focused() (row, bool) { + if e.cursor < 0 || e.cursor >= len(e.rows) { + return row{}, false + } + return e.rows[e.cursor], true +} + +func (e *ConfigEditor) move(delta int) { + i := e.cursor + for { + i += delta + if i < 0 || i >= len(e.rows) { + return + } + if e.rows[i].kind != rowSection { + break + } + } + e.setCursor(i) +} + +// jumpEdge moves to the first or last selectable row. +func (e *ConfigEditor) jumpEdge(last bool) { + for i := range e.rows { + index := i + if last { + index = len(e.rows) - 1 - i + } + if e.rows[index].kind != rowSection { + e.setCursor(index) + return + } + } +} + +// setCursor moves the cursor, collapsing an expanded list when the cursor +// leaves it. Row indices shift on collapse, so the target key is captured +// first. +func (e *ConfigEditor) setCursor(index int) { + if index < 0 || index >= len(e.rows) { + return + } + key := e.rows[index].key + if e.expanded != "" { + if base, _, ok := splitListKey(key); !ok || base != e.expanded { + e.expanded = "" + e.rebuild() + } + } + e.anchor(key, index) +} + +// refocus rebuilds rows and puts the cursor back on key. +func (e *ConfigEditor) refocus(key string) { + e.rebuild() + e.anchor(key, e.cursor) +} + +func (e *ConfigEditor) anchor(key string, fallback int) { + if len(e.rows) == 0 { + e.cursor = 0 + return + } + for i := range e.rows { + if e.rows[i].key == key { + e.cursor = i + return + } + } + fallback = min(max(fallback, 0), len(e.rows)-1) + for fallback > 0 && e.rows[fallback].kind == rowSection { + fallback-- + } + e.cursor = fallback +} + +func (e *ConfigEditor) rebuild() { + e.build() + e.cursor = min(max(e.cursor, 0), max(len(e.rows)-1, 0)) +} + +func (e *ConfigEditor) activate() { + r, ok := e.focused() + if !ok { + return + } + switch r.kind { + case rowAdd: + e.startEdit(r.key, "") + case rowItem: + e.startEdit(r.key, r.label) + case rowField: + switch r.field { + case kindList: + e.expand(r.key) + case kindBool, kindPlainBool: + e.toggleFocused() + case kindChoice: + e.openKeyPicker(r.label, r.key, r.options) + default: + e.startEdit(r.key, r.raw) + } + } +} + +func (e *ConfigEditor) expand(key string) { + e.expanded = key + e.rebuild() + if len(e.listItems(key)) > 0 { + e.anchor(listItemKey(key, 0), e.cursor) + return + } + e.anchor(listAddKey(key), e.cursor) +} + +func (e *ConfigEditor) collapse() { + key := e.expanded + e.expanded = "" + e.refocus(key) +} + +func (e *ConfigEditor) toggleFocused() { + r, ok := e.focused() + if !ok { + return + } + switch r.field { + case kindBool: + e.commit(r.key, cycleTri(r.raw)) + case kindPlainBool: + e.commit(r.key, onOff(r.raw != triOn)) + } +} + +// cycleFocused steps a choice or toggle with ←/→ so the common edits never need +// a modal. +func (e *ConfigEditor) cycleFocused(delta int) { + r, ok := e.focused() + if !ok || r.kind != rowField { + return + } + switch r.field { + case kindBool: + e.commit(r.key, cycleTri(r.raw)) + case kindPlainBool: + e.commit(r.key, onOff(r.raw != triOn)) + case kindChoice: + if len(r.options) == 0 { + return + } + at := 0 + for i, o := range r.options { + if o.value == r.raw { + at = i + break + } + } + next := (at + delta + len(r.options)) % len(r.options) + e.commit(r.key, r.options[next].value) + } +} + +func (e *ConfigEditor) commit(key, value string) { + if err := e.apply(key, value); err != nil { + e.setError(err.Error()) + return + } + e.dirty = true + e.status, e.statusKind = "", statusNone + e.refocus(key) +} + +func (e *ConfigEditor) addModel() { + e.expanded = "" + e.doc.Models = append(e.doc.Models, project.ModelDoc{}) + if len(e.doc.Models) == 1 { + e.doc.Models[0].Default = true + } + e.dirty = true + index := len(e.doc.Models) - 1 + e.refocus(modelKey(index, fName)) + e.startEdit(modelKey(index, fName), "") + e.setStatus("new model — type a model id") +} + +func (e *ConfigEditor) deleteFocused() { + r, ok := e.focused() + if !ok { + return + } + if r.kind == rowItem { + _, index, _ := splitListKey(r.key) + e.removeListItem(r.listKey, index) + e.dirty = true + e.refocus(listAddKey(r.listKey)) + return + } + index, ok := modelOf(r) + if !ok { + return + } + name := e.doc.Models[index].Name + if strings.TrimSpace(name) == "" { + name = "this model" + } else { + name = "model " + name + } + e.confirm = &confirmState{ + title: "Delete " + name + "?", + body: "Nothing is written until you save.", + danger: true, + yes: false, // destructive: Enter keeps the model + run: func(*components.EventContext) { + e.deleteModel(index) + }, + } +} + +func (e *ConfigEditor) deleteModel(index int) { + if index < 0 || index >= len(e.doc.Models) { + return + } + e.doc.Models = append(e.doc.Models[:index:index], e.doc.Models[index+1:]...) + e.dirty = true + // Keep one default: with none marked, the loader silently starts from the + // first entry — make that the explicit choice here instead. + defaults := 0 + for _, m := range e.doc.Models { + if m.Default { + defaults++ + } + } + if defaults == 0 && len(e.doc.Models) > 0 { + e.doc.Models[0].Default = true + } + if next := min(index, len(e.doc.Models)-1); next >= 0 { + e.refocus(modelKey(next, fName)) + return + } + e.refocus(keySkillPath) +} + +// modelOf reports the model index a row belongs to. +func modelOf(r row) (int, bool) { + switch r.kind { + case rowModel: + return r.model, true + case rowField: + if index, _, ok := splitModelKey(r.key); ok { + return index, true + } + } + return 0, false +} + +func (e *ConfigEditor) save() { + if err := e.doc.Validate(); err != nil { + e.setError(err.Error()) + return + } + if err := e.doc.Save(e.path); err != nil { + e.setError(err.Error()) + return + } + e.dirty = false + e.status, e.statusKind = "saved "+e.path, statusSuccess +} + +func (e *ConfigEditor) requestQuit(ctx *components.EventContext) { + if !e.dirty { + ctx.Quit = true + return + } + e.confirm = &confirmState{ + title: "Discard unsaved changes?", + body: "The file on disk still holds the last saved version.", + danger: true, + yes: false, + run: func(ctx *components.EventContext) { + ctx.Quit = true + }, + } +} + +func (e *ConfigEditor) handleConfirmKey(ctx *components.EventContext, k xui.KeyEvent) { + st := e.confirm + switch k.Code { + case xui.KeyLeft, xui.KeyUp: + st.yes = false + case xui.KeyRight, xui.KeyDown, xui.KeyTab: + st.yes = true + case xui.KeyEnter: + e.confirm = nil + if st.yes && st.run != nil { + st.run(ctx) + } + case xui.KeyEscape: + e.confirm = nil + case xui.KeyRune: + switch k.Rune { + case 'y', 'Y': + e.confirm = nil + if st.run != nil { + st.run(ctx) + } + case 'n', 'N': + e.confirm = nil + case 'h', 'H', 'k', 'K': + st.yes = false + case 'l', 'L', 'j', 'J': + st.yes = true + } + default: + ctx.Consume = true + return + } + ctx.ConsumeAndRedraw() +} + +func (e *ConfigEditor) startEdit(key, raw string) { + buf := []rune(raw) + e.edit = &editState{key: key, buf: buf, cur: len(buf)} + e.refocus(key) +} + +func (e *ConfigEditor) handleEditKey(ctx *components.EventContext, k xui.KeyEvent) { + switch k.Code { + case xui.KeyEnter: + e.commitEdit() + case xui.KeyEscape: + e.edit = nil + case xui.KeyBackspace: + e.deleteBefore() + case xui.KeyDelete: + e.deleteAt() + case xui.KeyLeft: + e.moveCursor(-1) + case xui.KeyRight: + e.moveCursor(1) + case xui.KeyHome: + e.edit.cur = 0 + case xui.KeyEnd: + e.edit.cur = len(e.edit.buf) + case xui.KeyRune: + if k.Mods.Has(xui.ModCtrl) { + e.handleEditCtrl(k.Rune) + break + } + if k.Mods.Has(xui.ModAlt) { + ctx.Consume = true + return + } + text := k.Text + if text == "" && k.Rune >= 0x20 { + text = string(k.Rune) + } + e.insert(text) + default: + ctx.Consume = true + return + } + ctx.ConsumeAndRedraw() +} + +func (e *ConfigEditor) handleEditCtrl(r rune) { + switch unicode.ToLower(r) { + case 'a': + e.edit.cur = 0 + case 'e': + e.edit.cur = len(e.edit.buf) + case 'u': + e.edit.buf = append([]rune(nil), e.edit.buf[e.edit.cur:]...) + e.edit.cur = 0 + case 'k': + e.edit.buf = e.edit.buf[:e.edit.cur] + case 'w': + for e.edit.cur > 0 && unicode.IsSpace(e.edit.buf[e.edit.cur-1]) { + e.deleteBefore() + } + for e.edit.cur > 0 && !unicode.IsSpace(e.edit.buf[e.edit.cur-1]) { + e.deleteBefore() + } + } +} + +func (e *ConfigEditor) insert(text string) { + if text == "" || e.edit == nil { + return + } + runes := []rune(text) + cur := e.edit.cur + out := make([]rune, 0, len(e.edit.buf)+len(runes)) + out = append(out, e.edit.buf[:cur]...) + out = append(out, runes...) + out = append(out, e.edit.buf[cur:]...) + e.edit.buf = out + e.edit.cur = cur + len(runes) +} + +func (e *ConfigEditor) deleteBefore() { + if e.edit == nil || e.edit.cur == 0 { + return + } + e.edit.buf = append(e.edit.buf[:e.edit.cur-1], e.edit.buf[e.edit.cur:]...) + e.edit.cur-- +} + +func (e *ConfigEditor) deleteAt() { + if e.edit == nil || e.edit.cur >= len(e.edit.buf) { + return + } + e.edit.buf = append(e.edit.buf[:e.edit.cur], e.edit.buf[e.edit.cur+1:]...) +} + +func (e *ConfigEditor) moveCursor(delta int) { + if e.edit == nil { + return + } + e.edit.cur = min(max(e.edit.cur+delta, 0), len(e.edit.buf)) +} + +func (e *ConfigEditor) commitEdit() { + st := e.edit + if st == nil { + return + } + key := st.key + value := string(st.buf) + if err := e.apply(key, value); err != nil { + e.setError(err.Error()) + return + } + e.edit = nil + e.dirty = true + e.status, e.statusKind = "", statusNone + if base, index, ok := splitListKey(key); ok && index < 0 { + e.refocus(listItemKey(base, len(e.listItems(base))-1)) + return + } + e.refocus(key) +} + +func (e *ConfigEditor) fetchModels() { + if e.list == nil { + e.setError("model list is unavailable") + return + } + r, ok := e.focused() + if !ok { + return + } + index, ok := modelOf(r) + if !ok { + e.setStatus("move to a model first") + return + } + m := e.modelAt(index) + if m == nil { + return + } + e.fetchMu.Lock() + if e.fetch.active { + e.fetchMu.Unlock() + e.setStatus("already fetching models…") + return + } + e.fetch = fetchState{active: true, index: index, name: m.Name} + e.fetchMu.Unlock() + + baseURL, apiKey, api, name := m.BaseURL, m.APIKey, m.API, m.Name + e.setStatus("fetching models…") + go func() { + ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout) + defer cancel() + ids, err := e.list(ctx, baseURL, apiKey, api, name) + e.fetchMu.Lock() + e.fetch = fetchState{done: true, index: index, name: name, ids: ids, err: err} + e.fetchMu.Unlock() + if e.redraw != nil { + e.redraw() + } + }() +} + +// pollFetch adopts a finished background fetch. It runs on the UI goroutine +// inside Draw, next to the frame that shows the "fetching" status. +func (e *ConfigEditor) pollFetch() { + e.fetchMu.Lock() + if !e.fetch.done { + e.fetchMu.Unlock() + return + } + result := e.fetch + e.fetch = fetchState{} + e.fetchMu.Unlock() + + if result.err != nil { + e.setError("model list: " + result.err.Error()) + return + } + if len(result.ids) == 0 { + e.setError("model list: the provider returned no models") + return + } + m := e.modelAt(result.index) + if m == nil || m.Name != result.name { + e.setError("model list: the model changed while fetching — press f again") + return + } + options := make([]option, 0, len(result.ids)) + for _, id := range result.ids { + options = append(options, opt(id, id, "advertised by the provider")) + } + e.openKeyPicker("Model list", modelKey(result.index, fName), options) +} + +func (e *ConfigEditor) openKeyPicker(title, key string, options []option) { + if len(options) == 0 { + return + } + current := e.rawFor(key) + items := make([]listpicker.Item, 0, len(options)) + for _, o := range options { + badge := "" + if o.value == current { + badge = "current" + } + items = append(items, listpicker.Item{ID: o.value, Primary: o.label, Badge: badge, Detail: o.desc}) + } + e.picker = listpicker.Picker{ + Theme: e.theme, + OnAccept: func(item listpicker.Item) { e.commit(key, item.ID) }, + } + e.picker.Show(items, listpicker.ShowConfig{ + Title: title, + PrimaryWidth: pickerPrimaryWidth, + LeadingWidth: 1, + }) +} + +func (e *ConfigEditor) handleMouse(ctx *components.EventContext, m xui.MouseEvent) { + // Modals own the pointer: while one is up the form behind it must not move. + // A press dismisses the picker (Esc-equivalent); the confirm modal is + // keyboard-only. + if e.picker.Open { + if m.Action == xui.MousePress { + e.picker.Hide() + ctx.ConsumeAndRedraw() + return + } + ctx.Consume = true + return + } + if e.confirm != nil { + ctx.Consume = true + return + } + switch { + case m.Button == xui.MouseWheelUp: + e.move(-max(m.Wheel, 1)) + case m.Button == xui.MouseWheelDown: + e.move(max(m.Wheel, 1)) + case m.Action == xui.MousePress && m.Button == xui.MouseLeft: + // Clicking away from an inline edit drops it, matching Esc: a click must + // never keep typing into the field the caret just left. + if e.edit != nil { + e.edit = nil + } + if index := e.rowAt(m.Y); index >= 0 { + e.setCursor(index) + } + default: + ctx.Consume = true + return + } + ctx.ConsumeAndRedraw() +} + +// rowAt maps a screen row to a selectable row index, or -1. +func (e *ConfigEditor) rowAt(y int) int { + if e.page <= 0 || y < e.rowsTop || y >= e.rowsTop+e.page { + return -1 + } + index := e.scroll + (y - e.rowsTop) + if index < 0 || index >= len(e.rows) || e.rows[index].kind == rowSection { + return -1 + } + return index +} + +func (e *ConfigEditor) setError(msg string) { + e.status, e.statusKind = msg, statusError +} + +func (e *ConfigEditor) setStatus(msg string) { + e.status, e.statusKind = msg, statusInfo +} + +func (e *ConfigEditor) themeOrDefault() components.Theme { + if e.theme.Border.Fg.Kind == 0 && e.theme.Foreground.Fg.Kind == 0 { + return components.DefaultTheme() + } + return e.theme +} diff --git a/internal/tui/configui/editor_test.go b/internal/tui/configui/editor_test.go new file mode 100644 index 00000000..8df96f1f --- /dev/null +++ b/internal/tui/configui/editor_test.go @@ -0,0 +1,558 @@ +package configui + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pulseaiclub/xui" + + "github.com/pulseaiclub/phi/internal/components" + "github.com/pulseaiclub/phi/internal/project" +) + +// newTestEditor builds an editor over a fake home, so paths in the chrome stay +// short and "~" shortening is exercised. +func newTestEditor(t *testing.T, doc *project.ConfigDoc) *ConfigEditor { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + e := New(doc, filepath.Join(home, ".phi", "config.yaml"), filepath.Join(home, ".phi", "skills"), + components.DarkTheme(), nil, nil) + require.NotEmpty(t, e.rows) + return e +} + +// focus moves the cursor onto key, failing the test when the row is missing. +func focus(t *testing.T, e *ConfigEditor, key string) { + t.Helper() + for i := range e.rows { + if e.rows[i].key == key { + e.setCursor(i) + require.Equal(t, key, e.rows[e.cursor].key) + return + } + } + t.Fatalf("no row with key %q", key) +} + +func press(e *ConfigEditor, code xui.KeyCode, r rune) *components.EventContext { + ctx := &components.EventContext{} + e.Handle(ctx, xui.KeyEvent{Code: code, Rune: r, Press: true}) + return ctx +} + +func typeRune(e *ConfigEditor, r rune) { + press(e, xui.KeyRune, r) +} + +func sampleDoc() *project.ConfigDoc { + return &project.ConfigDoc{ + Models: []project.ModelDoc{ + {Name: "model-a", APIKey: "key-a", BaseURL: "https://a.example/v1", Default: true}, + {Name: "model-b", APIKey: "key-b", BaseURL: "https://b.example/v1"}, + }, + } +} + +func TestBuildRowsCoverEverySection(t *testing.T) { + e := newTestEditor(t, sampleDoc()) + + var sections []string + for _, r := range e.rows { + if r.kind == rowSection { + sections = append(sections, r.title) + } + } + assert.Equal(t, []string{"Models", "Skills", "Permissions", "Bash", "Sub-agents"}, sections) + + // Every row key must be addressable, and the first model's default badge + // must follow the document. + require.Equal(t, "default", e.rows[1].badge) + assert.Equal(t, "model-a", e.rows[1].name) + assert.Empty(t, e.rows[2].badge) +} + +func TestCursorSkipsSectionHeaders(t *testing.T) { + e := newTestEditor(t, sampleDoc()) + for range len(e.rows) * 2 { + e.move(1) + require.NotEqual(t, rowSection, e.rows[e.cursor].kind, "cursor landed on a section header") + } + for range len(e.rows) * 2 { + e.move(-1) + require.NotEqual(t, rowSection, e.rows[e.cursor].kind, "cursor landed on a section header") + } + + e.jumpEdge(true) + require.NotEqual(t, rowSection, e.rows[e.cursor].kind) + e.jumpEdge(false) + require.NotEqual(t, rowSection, e.rows[e.cursor].kind) +} + +func TestEditModelNameRejectsEmpty(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, modelKey(0, fName)) + press(e, xui.KeyEnter, 0) + require.NotNil(t, e.edit, "Enter must open the inline editor") + + press(e, xui.KeyBackspace, 0) + for range len("model-a") - 1 { + press(e, xui.KeyBackspace, 0) + } + press(e, xui.KeyEnter, 0) + require.NotNil(t, e.edit, "an empty name must keep the editor open") + assert.Equal(t, "model-a", doc.Models[0].Name) + assert.Equal(t, statusError, e.statusKind) + + typeRune(e, 'x') + press(e, xui.KeyEnter, 0) + assert.Nil(t, e.edit) + assert.Equal(t, "x", doc.Models[0].Name) + assert.True(t, e.Dirty()) +} + +func TestEditKeyPromptUsesNoCLIValues(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, modelKey(0, fKey)) + press(e, xui.KeyEnter, 0) + require.NotNil(t, e.edit) + assert.Equal(t, "key-a", string(e.edit.buf), "editing starts from the stored value") + + press(e, xui.KeyEscape, 0) + assert.Nil(t, e.edit) + assert.False(t, e.Dirty(), "canceling an edit must not dirty the document") +} + +func TestCycleChoiceAndTriState(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, keyPermMode) + assert.Nil(t, doc.Permissions) + press(e, xui.KeyRight, 0) + require.NotNil(t, doc.Permissions) + assert.Equal(t, "interactive", doc.Permissions.Mode) + press(e, xui.KeyLeft, 0) + assert.Empty(t, doc.Permissions.Mode) + + focus(t, e, modelKey(0, fImage)) + press(e, xui.KeyRight, 0) + require.NotNil(t, doc.Models[0].ImageEnabled) + assert.True(t, *doc.Models[0].ImageEnabled) + press(e, xui.KeyRight, 0) + require.NotNil(t, doc.Models[0].ImageEnabled) + assert.False(t, *doc.Models[0].ImageEnabled) + press(e, xui.KeyRight, 0) + assert.Nil(t, doc.Models[0].ImageEnabled, "the third step must return to auto (absent)") +} + +func TestDefaultModelIsExclusive(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, modelKey(1, fDeflt)) + press(e, xui.KeyEnter, 0) + assert.False(t, doc.Models[0].Default) + assert.True(t, doc.Models[1].Default) +} + +func TestAddAndDeleteModel(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + typeRune(e, 'a') + require.Len(t, doc.Models, 3) + assert.Equal(t, "new model — type a model id", e.status) + require.NotNil(t, e.edit, "adding a model opens the name editor") + typeRune(e, 'c') + press(e, xui.KeyEnter, 0) + assert.Equal(t, "c", doc.Models[2].Name) + + focus(t, e, modelKey(2, fName)) + typeRune(e, 'd') + require.NotNil(t, e.confirm, "deleting a model must confirm first") + assert.False(t, e.confirm.yes, "a destructive confirm must default to No") + assert.Len(t, doc.Models, 3) + + press(e, xui.KeyEnter, 0) // Enter takes the default: keep the model. + assert.Len(t, doc.Models, 3) + + typeRune(e, 'd') + typeRune(e, 'y') + assert.Nil(t, e.confirm) + assert.Len(t, doc.Models, 2) + + // Deleting the default promotes the next model, so the file never relies on + // the loader's implicit first-entry rule. + focus(t, e, modelKey(0, fName)) + typeRune(e, 'd') + typeRune(e, 'y') + require.Len(t, doc.Models, 1) + assert.True(t, doc.Models[0].Default) +} + +func TestDeleteModelCanBeCancelled(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, modelKey(0, fName)) + typeRune(e, 'd') + press(e, xui.KeyEscape, 0) + assert.Nil(t, e.confirm) + assert.Len(t, doc.Models, 2) +} + +func TestBashRulesInheritUntilEdited(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + // Collapsed, the field says the rules are the built-in ones. + focus(t, e, keyPermBashAllow) + r, _ := e.focused() + assert.True(t, r.inherited) + assert.Contains(t, r.display, "built-in") + + // Expanding lists the effective rules and offers a new-pattern row. + press(e, xui.KeyEnter, 0) + assert.Equal(t, keyPermBashAllow, e.expanded) + require.Greater(t, len(e.rows), len(e.listItems(keyPermBashAllow))) + assert.Nil(t, doc.Permissions, "looking at the rules must not write them") + r, _ = e.focused() + assert.Equal(t, rowItem, r.kind) + + // Editing one rule materializes the whole list, built-ins included. + press(e, xui.KeyEnter, 0) + require.NotNil(t, e.edit) + assert.Equal(t, e.listItems(keyPermBashAllow)[0], string(e.edit.buf)) + press(e, xui.KeyEnd, 0) + typeRune(e, 'x') + press(e, xui.KeyEnter, 0) + require.NotNil(t, doc.Permissions) + require.NotNil(t, doc.Permissions.Bash) + require.NotNil(t, doc.Permissions.Bash.Allow) + assert.Len(t, *doc.Permissions.Bash.Allow, len(e.listItems(keyPermBashAllow))) +} + +func TestAddBashRuleRow(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, keyPermBashAllow) + press(e, xui.KeyEnter, 0) + focus(t, e, listAddKey(keyPermBashAllow)) + press(e, xui.KeyEnter, 0) + require.NotNil(t, e.edit) + + for _, r := range "^make " { + typeRune(e, r) + } + press(e, xui.KeyEnter, 0) + require.NotNil(t, doc.Permissions.Bash.Allow) + assert.Equal(t, "^make ", (*doc.Permissions.Bash.Allow)[len(*doc.Permissions.Bash.Allow)-1]) + // The cursor lands on the row it just created. + assert.Equal(t, listItemKey(keyPermBashAllow, len(*doc.Permissions.Bash.Allow)-1), e.rows[e.cursor].key) +} + +func TestDeleteBashRule(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, keyPermBashDeny) + press(e, xui.KeyEnter, 0) + before := len(e.listItems(keyPermBashDeny)) + press(e, xui.KeyEnter, 0) // edit + press(e, xui.KeyEscape, 0) + typeRune(e, 'd') + require.NotNil(t, doc.Permissions.Bash.Deny) + assert.Len(t, *doc.Permissions.Bash.Deny, before-1) +} + +func TestSaveWritesFileAndBackup(t *testing.T) { + home := t.TempDir() + dir := filepath.Join(home, ".phi") + require.NoError(t, os.MkdirAll(dir, 0o755)) + path := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(path, []byte("models:\n - name: old\n api_key: k\n"), 0o600)) + + doc, err := project.ReadConfigDoc(path) + require.NoError(t, err) + e := New(doc, path, "/skills", components.DarkTheme(), nil, nil) + + focus(t, e, modelKey(0, fName)) + press(e, xui.KeyEnter, 0) + press(e, xui.KeyEnd, 0) + for _, r := range "-2" { + typeRune(e, r) + } + press(e, xui.KeyEnter, 0) + + press(e, xui.KeyRune, 's') + assert.Equal(t, statusSuccess, e.statusKind, e.status) + assert.False(t, e.Dirty()) + + saved, err := project.ReadConfigDoc(path) + require.NoError(t, err) + require.Len(t, saved.Models, 1) + assert.Equal(t, "old-2", saved.Models[0].Name) + + backup, err := os.ReadFile(path + ".bak") + require.NoError(t, err) + assert.Contains(t, string(backup), "name: old") + + // The written file must load through the runtime parser. + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + p, err := project.Discover("") + require.NoError(t, err) + require.NoError(t, p.LoadConfig()) + assert.Equal(t, "old-2", p.Config().Model().Name) +} + +func TestSaveRefusesInvalidDocument(t *testing.T) { + doc := &project.ConfigDoc{Models: []project.ModelDoc{{Name: "", APIKey: "k"}}} + e := New(doc, filepath.Join(t.TempDir(), "config.yaml"), "/skills", components.DarkTheme(), nil, nil) + + press(e, xui.KeyRune, 's') + assert.Equal(t, statusError, e.statusKind) + assert.Contains(t, e.status, "no name") +} + +func TestQuitGuardsUnsavedChanges(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + ctx := press(e, xui.KeyEscape, 0) + assert.True(t, ctx.Quit, "a clean editor quits on Esc") + + focus(t, e, keyPermWorkspace) + typeRune(e, ' ') // toggle a bool through the space shortcut + require.True(t, e.Dirty()) + + ctx = press(e, xui.KeyEscape, 0) + require.NotNil(t, e.confirm) + assert.False(t, ctx.Quit) + + ctx = press(e, xui.KeyEscape, 0) // cancel the confirm + assert.Nil(t, e.confirm) + assert.False(t, ctx.Quit) + + press(e, xui.KeyEscape, 0) + ctx = press(e, xui.KeyRune, 'y') // discard + assert.True(t, ctx.Quit) +} + +func TestEscapeClosesExpandedListFirst(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, keyPermBashAllow) + press(e, xui.KeyEnter, 0) + require.NotEmpty(t, e.expanded) + + ctx := press(e, xui.KeyEscape, 0) + assert.Empty(t, e.expanded) + assert.False(t, ctx.Quit) + assert.Equal(t, keyPermBashAllow, e.rows[e.cursor].key) +} + +func TestChoicePickerCommitsSelection(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, keyAgentsExplore) + press(e, xui.KeyEnter, 0) + require.True(t, e.picker.Open) + + press(e, xui.KeyEnter, 0) // first row is "(inherit)". + assert.False(t, e.picker.Open) + require.NotNil(t, doc.Agents) + require.NotNil(t, doc.Agents.Models) + assert.Empty(t, doc.Agents.Models.Explore) + + focus(t, e, keyAgentsExplore) + press(e, xui.KeyEnter, 0) + require.True(t, e.picker.Open) + typeRune(e, 'b') // filter to model-b + press(e, xui.KeyEnter, 0) + assert.Equal(t, "model-b", doc.Agents.Models.Explore) +} + +func TestDrawPaintsFormAndStatus(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + ctx := components.DrawContext{Max: components.Size{Width: 100, Height: 48}, Method: xui.WidthUnicode} + surf := e.Draw(ctx) + text := components.SurfaceText(surf) + + assert.Contains(t, text, "phi config") + assert.Contains(t, text, "model-a") + assert.Contains(t, text, "~/.phi/config.yaml") + assert.Contains(t, text, "auto") + assert.Contains(t, text, "MODELS") + assert.Contains(t, text, "PERMISSIONS") + assert.Contains(t, text, "allow patterns") + assert.Contains(t, text, "built-in · ") + assert.Contains(t, text, "↑↓ move") + + // The caret follows the inline editor. + focus(t, e, modelKey(0, fName)) + press(e, xui.KeyEnter, 0) + surf = e.Draw(ctx) + require.NotNil(t, surf.Cursor) + assert.Positive(t, surf.Cursor.Y) + + // An unsaved document is called out in the title bar. + typeRune(e, 'x') + press(e, xui.KeyEnter, 0) + surf = e.Draw(ctx) + assert.Contains(t, components.SurfaceText(surf), "●") +} + +func TestDrawIsStableOnTinyScreens(t *testing.T) { + e := newTestEditor(t, sampleDoc()) + for _, size := range []components.Size{{Width: 1, Height: 1}, {Width: 20, Height: 4}, {Width: 40, Height: 6}} { + surf := e.Draw(components.DrawContext{Max: size, Method: xui.WidthUnicode}) + assert.Equal(t, size, surf.Size) + } +} + +func TestMouseWheelMovesAndClickSelects(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + ctx := components.DrawContext{Max: components.Size{Width: 100, Height: 32}, Method: xui.WidthUnicode} + e.Draw(ctx) + + start := e.cursor + ev := &components.EventContext{} + e.Handle(ev, xui.MouseEvent{Action: xui.MousePress, Button: xui.MouseWheelDown, Wheel: 2}) + assert.Equal(t, min(start+2, len(e.rows)-1), e.cursor) + + // A click on a selectable row selects it. + y := e.rowsTop + (1 - e.scroll) + require.GreaterOrEqual(t, y, e.rowsTop) + ev = &components.EventContext{} + e.Handle(ev, xui.MouseEvent{X: 6, Y: y, Action: xui.MousePress, Button: xui.MouseLeft}) + assert.Equal(t, 1, e.cursor) +} + +func TestMouseDropsInlineEditOnClickAway(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + ctx := components.DrawContext{Max: components.Size{Width: 100, Height: 32}, Method: xui.WidthUnicode} + e.Draw(ctx) + + // Open the inline editor on the first model's name. + focus(t, e, modelKey(0, fName)) + e.activate() + require.NotNil(t, e.edit) + typeRune(e, 'X') + + // Click a different row: the edit is dropped (no silent commit), cursor + // moves to the clicked row, and the buffer is gone. + ev := &components.EventContext{} + e.Handle(ev, xui.MouseEvent{X: 6, Y: e.rowsTop + (3 - e.scroll), Action: xui.MousePress, Button: xui.MouseLeft}) + assert.Nil(t, e.edit) + assert.NotEqual(t, modelKey(0, fName), e.rows[e.cursor].key) +} + +func TestMouseDismissesPickerAndIgnoresConfirm(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + ctx := components.DrawContext{Max: components.Size{Width: 100, Height: 32}, Method: xui.WidthUnicode} + e.Draw(ctx) + + // A press while the picker is open closes it without touching the form. + focus(t, e, modelKey(0, fAPI)) + e.activate() // opens the api picker + require.True(t, e.picker.Open) + before := e.cursor + ev := &components.EventContext{} + e.Handle(ev, xui.MouseEvent{X: 5, Y: 5, Action: xui.MousePress, Button: xui.MouseLeft}) + assert.False(t, e.picker.Open) + assert.Equal(t, before, e.cursor) + + // A press while the confirm modal is up is ignored entirely. + e.dirty = true // requestQuit only shows the modal when dirty + e.requestQuit(&components.EventContext{}) + require.NotNil(t, e.confirm) + cur := e.cursor + ev = &components.EventContext{} + e.Handle(ev, xui.MouseEvent{X: 5, Y: 5, Action: xui.MousePress, Button: xui.MouseLeft}) + require.NotNil(t, e.confirm) + assert.Equal(t, cur, e.cursor) +} + +func TestSkillsFallbackIsShownWhenUnset(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + focus(t, e, keySkillPath) + r, _ := e.focused() + assert.Equal(t, "~/.phi/skills", r.display) + assert.Equal(t, styleMuted, r.style) + // Empty skill_path stays absent from the document. + press(e, xui.KeyEnter, 0) + press(e, xui.KeyEnter, 0) + assert.Nil(t, doc.SkillPath) +} + +func TestFetchDiscardedWhenModelMoves(t *testing.T) { + doc := sampleDoc() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + list := ModelLister(func(_ context.Context, _, _, _, _ string) ([]string, error) { + return []string{"alpha", "beta"}, nil + }) + e := New(doc, filepath.Join(home, ".phi", "config.yaml"), filepath.Join(home, ".phi", "skills"), + components.DarkTheme(), list, func() {}) + + // Start a fetch on model 0, then delete model 0 before the result lands so + // model 1 shifts into its slot. The fetch's captured index/name pair must no + // longer match, and the picker must not open for the wrong model. + focus(t, e, modelKey(0, fName)) + e.fetchModels() + require.True(t, e.fetch.active) + + // Drain the background fetch synchronously: it only touches fetchMu state. + require.Eventually(t, func() bool { + e.fetchMu.Lock() + done := e.fetch.done + e.fetchMu.Unlock() + return done + }, time.Second, time.Millisecond) + + // Delete model 0 (default promotion keeps model 1 as the new default). + e.deleteModel(0) + require.Len(t, doc.Models, 1) + assert.Equal(t, "model-b", doc.Models[0].Name) + + e.pollFetch() + assert.False(t, e.picker.Open, "picker must not open for a model that moved") + assert.Contains(t, e.status, "changed while fetching") +} + +func TestHumanTokens(t *testing.T) { + cases := map[int]string{ + 0: "", + 999: "999", + 128000: "128k", + 1000000: "1M", + 1280000: "1.3M", + 2000000: "2M", + } + for n, want := range cases { + assert.Equal(t, want, humanTokens(n), "n=%d", n) + } +} diff --git a/internal/tui/configui/model.go b/internal/tui/configui/model.go new file mode 100644 index 00000000..385c0cff --- /dev/null +++ b/internal/tui/configui/model.go @@ -0,0 +1,921 @@ +package configui + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/pulseaiclub/phi/internal/llm" + "github.com/pulseaiclub/phi/internal/permission" + "github.com/pulseaiclub/phi/internal/project" + "github.com/pulseaiclub/phi/internal/project/model" +) + +// Field keys are stable ids: the cursor, the list editor, and tests all address +// fields by key, never by row index, so a rebuild may reorder rows freely. +const ( + keySkillPath = "skill_path" + + keyPermMode = "perm.mode" + keyPermWorkspace = "perm.workspace_only_writes" + keyPermTimeout = "perm.ask_timeout_sec" + keyPermDangerous = "perm.dangerously_allow_all" + keyPermBashDefault = "perm.bash.default" + keyPermBashAllow = "perm.bash.allow" + keyPermBashDeny = "perm.bash.deny" + + keyAgentsEnabled = "agents.enabled" + keyAgentsExplore = "agents.models.explore" + keyAgentsReview = "agents.models.review" + keyAgentsWorker = "agents.models.worker" +) + +// Model fields live under model... +const ( + fName = "name" + fKey = "api_key" + fURL = "base_url" + fAPI = "api" + fCtx = "context_window" + fImage = "image_enabled" + fThink = "think_level" + fDeflt = "default" +) + +// defaultBaseURL mirrors the endpoint project.parseConfigFile falls back to. +const defaultBaseURL = "https://api.openai.com/v1" + +// Tri-state toggle encodings. A bool field is written as absent (auto, the +// loader default), on, or off. +const ( + triAuto = "auto" + triOn = "on" + triOff = "off" +) + +func modelKey(index int, field string) string { + return "model." + strconv.Itoa(index) + "." + field +} + +// splitModelKey parses model... +func splitModelKey(key string) (index int, field string, ok bool) { + rest, found := strings.CutPrefix(key, "model.") + if !found { + return 0, "", false + } + rawIndex, field, found := strings.Cut(rest, ".") + if !found { + return 0, "", false + } + index, err := strconv.Atoi(rawIndex) + if err != nil || index < 0 { + return 0, "", false + } + return index, field, true +} + +func listItemKey(key string, index int) string { + return key + "#" + strconv.Itoa(index) +} + +func listAddKey(key string) string { return key + "#new" } + +// splitListKey parses #, where index -1 marks the add row. +func splitListKey(key string) (field string, index int, ok bool) { + base, suffix, found := strings.Cut(key, "#") + if !found { + return "", 0, false + } + if suffix == "new" { + return base, -1, true + } + index, err := strconv.Atoi(suffix) + if err != nil || index < 0 { + return "", 0, false + } + return base, index, true +} + +type rowKind int + +const ( + rowSection rowKind = iota + rowModel + rowField + rowItem + rowAdd +) + +type fieldKind int + +const ( + kindText fieldKind = iota + kindSecret + kindInt + kindBool // tri-state: auto / on / off + kindPlainBool // plain bool with a default of off + kindChoice + kindList +) + +// option is one choice value. An empty value drops the key from the file, +// which is how the loader's "use the default" state is expressed. +type option struct { + value string + label string + desc string +} + +func opt(value, label, desc string) option { return option{value: value, label: label, desc: desc} } + +// valueStyle picks a semantic color for a rendered value. +type valueStyle int + +const ( + styleValue valueStyle = iota + styleMuted + styleOn + styleWarn +) + +// row is one painted line of the form. Rows are a view model: build rebuilds +// them from the document, and apply writes a single field back. +type row struct { + kind rowKind + key string + + // rowSection + title string + note string + + // rowModel + model int + name string + badge string + + // rowField / rowItem / rowAdd + field fieldKind + label string + raw string + display string + style valueStyle + hint string + options []option + items []string + listKey string // owning field key for rowItem / rowAdd + inherited bool // list rows showing built-in rules rather than file rules +} + +// fieldSpec declares one editable field; fieldRow renders it against the +// document. +type fieldSpec struct { + key string + label string + kind fieldKind + options []option + placeholder string + // defaultOn is the loader's value when the bool key is absent. + defaultOn bool +} + +var ( + apiOptions = []option{ + opt("", "auto", "infer from the model name"), + opt(string(llm.OpenAI), "OpenAI", "chat completions"), + opt(string(llm.OpenAIResponses), "OpenAI Responses", "responses API"), + opt(string(llm.Anthropic), "Anthropic", "messages API"), + opt(string(llm.Gemini), "Gemini", "generateContent"), + } + thinkOptions = []option{ + opt("", "auto", "model or preset default"), + opt(string(llm.Off), "off", "no reasoning"), + opt(string(llm.Minimal), "minimal", "shortest reasoning"), + opt(string(llm.Low), "low", "light reasoning"), + opt(string(llm.Medium), "medium", "balanced"), + opt(string(llm.High), "high", "deep reasoning"), + opt(string(llm.XHigh), "xhigh", "deeper still"), + opt(string(llm.Max), "max", "provider maximum"), + } + permModeOptions = []option{ + opt("", "auto", "interactive: ask before gated tools"), + opt(string(permission.ModeInteractive), "interactive", "ask before gated tools"), + opt(string(permission.ModeReadonly), "readonly", "deny writes, keep reads"), + opt(string(permission.ModeAutopilot), "autopilot", "fold ask to allow"), + opt(string(permission.ModeHeadlessStrict), "headless-strict", "fold ask to deny"), + } + bashDefaultOptions = []option{ + opt("", "auto", "policy default (ask)"), + opt("ask", "ask", "prompt before running"), + opt("allow", "allow", "run without asking"), + opt("deny", "deny", "never run"), + } +) + +// build rebuilds rows from the document. The caller re-anchors the cursor +// afterwards; see ConfigEditor.refocus. +func (e *ConfigEditor) build() { + e.rows = e.rows[:0] + e.buildModels() + e.addSection("Skills", "") + skills := displayPath(e.skills) + e.addField(e.fieldRow(fieldSpec{key: keySkillPath, label: "skill path", kind: kindText, placeholder: skills})) + e.addSection("Permissions", "") + e.addField(e.fieldRow(fieldSpec{key: keyPermMode, label: "mode", kind: kindChoice, options: permModeOptions})) + e.addField(e.fieldRow(fieldSpec{ + key: keyPermWorkspace, label: "workspace only writes", kind: kindBool, defaultOn: true, + })) + e.addField(e.fieldRow(fieldSpec{ + key: keyPermTimeout, label: "ask timeout", kind: kindInt, placeholder: strconv.Itoa(defaultTimeout()), + })) + e.addField(e.fieldRow(fieldSpec{key: keyPermDangerous, label: "allow everything", kind: kindBool})) + e.addSection("Bash", "") + e.addField(e.fieldRow(fieldSpec{ + key: keyPermBashDefault, label: "default", kind: kindChoice, options: bashDefaultOptions, + })) + e.addListField(keyPermBashAllow, "allow patterns") + e.addListField(keyPermBashDeny, "deny patterns") + e.addSection("Sub-agents", "") + e.addField(e.fieldRow(fieldSpec{key: keyAgentsEnabled, label: "enabled", kind: kindBool, defaultOn: true})) + for _, role := range []struct{ key, label string }{ + {keyAgentsExplore, "explore model"}, + {keyAgentsReview, "review model"}, + {keyAgentsWorker, "worker model"}, + } { + e.addField(e.fieldRow(fieldSpec{ + key: role.key, label: role.label, kind: kindChoice, options: e.modelRefOptions(), + })) + } +} + +func (e *ConfigEditor) buildModels() { + note := "none yet — press a to add one" + if n := len(e.doc.Models); n == 1 { + note = "1 model" + } else if n > 1 { + note = strconv.Itoa(n) + " models" + } + e.addSection("Models", note) + for i, m := range e.doc.Models { + badge := "" + if m.Default { + badge = "default" + } + name := m.Name + if strings.TrimSpace(name) == "" { + name = "unnamed model" + } + e.rows = append(e.rows, row{ + kind: rowModel, + key: modelKey(i, "header"), + model: i, + name: name, + badge: badge, + note: modelNote(m), + }) + for _, sp := range modelSpecs(m) { + sp.key = modelKey(i, sp.key) + e.addField(e.fieldRow(sp)) + } + } +} + +// modelSpecs lists the editable fields of one model entry. Placeholders show +// what the built-in preset would supply, so an empty field is never a mystery. +func modelSpecs(m project.ModelDoc) []fieldSpec { + preset, known := model.Lookup(m.Name) + baseURL := defaultBaseURL + contextWindow := 0 + image := false + if known { + if preset.Config.BaseURL != "" { + baseURL = preset.Config.BaseURL + } + contextWindow = preset.Config.ContextWindow + image = preset.Config.ImageEnabled + } + return []fieldSpec{ + {key: fName, label: "name", kind: kindText, placeholder: "model id"}, + {key: fKey, label: "api key", kind: kindSecret}, + {key: fURL, label: "base url", kind: kindText, placeholder: baseURL}, + {key: fAPI, label: "api", kind: kindChoice, options: apiOptions}, + {key: fCtx, label: "context window", kind: kindInt, placeholder: humanTokens(contextWindow)}, + {key: fImage, label: "image input", kind: kindBool, defaultOn: image}, + {key: fThink, label: "thinking", kind: kindChoice, options: thinkOptions}, + {key: fDeflt, label: "use by default", kind: kindPlainBool}, + } +} + +func (e *ConfigEditor) modelRefOptions() []option { + out := []option{opt("", "inherit", "parent session model")} + for _, m := range e.doc.Models { + if strings.TrimSpace(m.Name) == "" { + continue + } + out = append(out, opt(m.Name, m.Name, "configured model")) + } + return out +} + +func (e *ConfigEditor) addSection(title, note string) { + e.rows = append(e.rows, row{kind: rowSection, key: "§" + title, title: title, note: note}) +} + +func (e *ConfigEditor) addField(r row) { e.rows = append(e.rows, r) } + +// addListField appends a list field and, while it is expanded, its item rows. +func (e *ConfigEditor) addListField(key, label string) { + items := e.listItems(key) + inherited := !e.listSet(key) + display := plural(len(items), "pattern") + style := styleValue + if inherited { + display, style = "built-in · "+plural(len(items), "pattern"), styleMuted + } + e.addField(row{ + kind: rowField, key: key, field: kindList, label: label, + raw: strconv.Itoa(len(items)), display: display, style: style, + items: items, listKey: key, inherited: inherited, + }) + if e.expanded != key { + return + } + for i, item := range items { + style := styleValue + if inherited { + style = styleMuted + } + e.addField(row{ + kind: rowItem, key: listItemKey(key, i), label: item, style: style, + items: items, listKey: key, inherited: inherited, + }) + } + e.addField(row{ + kind: rowAdd, key: listAddKey(key), label: addLabel(inherited), + style: styleMuted, listKey: key, inherited: inherited, + }) +} + +// addLabel names the "add a rule" row; a list that is still inheriting gets an +// explicit warning that the first edit writes the built-in rules out too. +func addLabel(inherited bool) string { + if inherited { + return "+ new pattern (keeps built-in rules)" + } + return "+ new pattern" +} + +// fieldRow renders one field. It is the only place the value column is built, +// so styling stays consistent across sections. +func (e *ConfigEditor) fieldRow(sp fieldSpec) row { + raw := e.rawFor(sp.key) + r := row{kind: rowField, key: sp.key, field: sp.kind, label: sp.label, raw: raw, options: sp.options} + switch sp.kind { + case kindText: + r.display, r.style = raw, styleValue + if raw == "" { + r.display, r.style, r.hint = unset(sp.placeholder), styleMuted, sp.placeholder + } + case kindSecret: + if raw == "" { + r.display, r.style = "not set", styleWarn + } else { + r.display, r.style = maskSecret(raw), styleMuted + } + case kindInt: + r.display, r.style = raw, styleValue + if raw == "" { + r.display, r.style = unset(sp.placeholder), styleMuted + r.hint = "loader default" + } + case kindBool: + switch raw { + case triOn: + r.display, r.style = triOn, styleOn + case triOff: + r.display, r.style = triOff, styleValue + default: + r.display, r.style = triAuto, styleMuted + r.hint = "loader default: " + onOff(sp.defaultOn) + } + case kindPlainBool: + r.display, r.style = triOff, styleValue + if raw == triOn { + r.display, r.style = triOn, styleOn + } + case kindChoice: + label, desc := optionFor(sp.options, raw) + r.display, r.style, r.hint = label, styleValue, desc + if raw == "" { + r.style = styleMuted + } + } + return r +} + +// unset renders an empty field: the loader default when one is known, an +// em dash when the value only exists in the file. +func unset(fallback string) string { + if fallback == "" { + return "—" + } + return fallback +} + +func optionFor(options []option, value string) (label, desc string) { + for _, o := range options { + if o.value == value { + return o.label, o.desc + } + } + // A hand-edited file may hold a value the picker does not offer; show it + // rather than silently rewriting it. + return value, "not a known value" +} + +// rawFor returns the canonical string form of a field's value — the same +// encoding apply accepts back. "" means the key is absent. +func (e *ConfigEditor) rawFor(key string) string { + if index, field, ok := splitModelKey(key); ok { + m := e.modelAt(index) + if m == nil { + return "" + } + switch field { + case fName: + return m.Name + case fKey: + return m.APIKey + case fURL: + return m.BaseURL + case fAPI: + return m.API + case fCtx: + return intString(m.ContextWindow) + case fImage: + return triString(m.ImageEnabled) + case fThink: + return m.ThinkLevel + case fDeflt: + return onOff(m.Default) + } + return "" + } + if base, index, ok := splitListKey(key); ok { + items := e.listItems(base) + if index < 0 || index >= len(items) { + return "" + } + return items[index] + } + switch key { + case keySkillPath: + return stringValue(e.doc.SkillPath) + case keyPermMode: + return permString(e.doc, func(p *project.PermDoc) string { return p.Mode }) + case keyPermWorkspace: + return triString(permBool(e.doc, func(p *project.PermDoc) *bool { return p.WorkspaceOnlyWrites })) + case keyPermTimeout: + return intString(permInt(e.doc, func(p *project.PermDoc) *int { return p.AskTimeoutSec })) + case keyPermDangerous: + return triString(permBool(e.doc, func(p *project.PermDoc) *bool { return p.DangerouslyAllowAll })) + case keyPermBashDefault: + return bashString(e.doc, func(b *project.BashDoc) string { return b.Default }) + case keyAgentsEnabled: + return triString(agentsBool(e.doc, func(a *project.AgentsDoc) *bool { return a.Enabled })) + case keyAgentsExplore, keyAgentsReview, keyAgentsWorker: + return agentsString(e.doc, func(m *project.AgentsRoleModels) string { + switch key { + case keyAgentsExplore: + return m.Explore + case keyAgentsReview: + return m.Review + default: + return m.Worker + } + }) + } + return "" +} + +// apply writes one field. An error leaves the document untouched so the editor +// can report it and keep the user in the field. +func (e *ConfigEditor) apply(key, value string) error { + if index, field, ok := splitModelKey(key); ok { + return e.applyModel(index, field, value) + } + if base, index, ok := splitListKey(key); ok { + return e.applyListItem(base, index, value) + } + switch key { + case keySkillPath: + e.doc.SkillPath = optionalString(strings.TrimSpace(value)) + case keyPermMode: + perms(e.doc).Mode = strings.TrimSpace(value) + case keyPermWorkspace: + on, err := parseTri(value) + if err != nil { + return err + } + perms(e.doc).WorkspaceOnlyWrites = on + case keyPermTimeout: + secs, err := parsePositiveInt(value, "seconds") + if err != nil { + return err + } + perms(e.doc).AskTimeoutSec = secs + case keyPermDangerous: + on, err := parseTri(value) + if err != nil { + return err + } + perms(e.doc).DangerouslyAllowAll = on + case keyPermBashDefault: + bash(e.doc).Default = value + case keyAgentsEnabled: + on, err := parseTri(value) + if err != nil { + return err + } + agents(e.doc).Enabled = on + case keyAgentsExplore, keyAgentsReview, keyAgentsWorker: + role := strings.TrimPrefix(key, "agents.models.") + switch role { + case "explore": + agentsModels(e.doc).Explore = value + case "review": + agentsModels(e.doc).Review = value + case "worker": + agentsModels(e.doc).Worker = value + default: + return fmt.Errorf("unknown role %q", role) + } + default: + return fmt.Errorf("unknown field %q", key) + } + return nil +} + +func (e *ConfigEditor) applyModel(index int, field, value string) error { + m := e.modelAt(index) + if m == nil { + return fmt.Errorf("model %d no longer exists", index+1) + } + switch field { + case fName: + name := strings.TrimSpace(value) + if name == "" { + return errors.New("model name cannot be empty") + } + m.Name = name + case fKey: + m.APIKey = strings.TrimSpace(value) + case fURL: + m.BaseURL = strings.TrimSpace(value) + case fAPI: + m.API = value + case fCtx: + window, err := parsePositiveInt(value, "tokens") + if err != nil { + return err + } + m.ContextWindow = window + case fImage: + on, err := parseTri(value) + if err != nil { + return err + } + m.ImageEnabled = on + case fThink: + m.ThinkLevel = value + case fDeflt: + on := value == triOn + if on { + for i := range e.doc.Models { + if i != index { + e.doc.Models[i].Default = false + } + } + } + m.Default = on + default: + return fmt.Errorf("unknown model field %q", field) + } + return nil +} + +func (e *ConfigEditor) applyListItem(base string, index int, value string) error { + items := e.listItems(base) + // The pattern is stored verbatim: bash rules are regexes, and trimming one + // would quietly change what it matches. + rule := value + switch { + case index < 0: + if strings.TrimSpace(rule) == "" { + return errors.New("pattern cannot be empty") + } + items = append(items, rule) + case index < len(items): + if strings.TrimSpace(rule) == "" { + return errors.New("pattern cannot be empty") + } + items[index] = rule + default: + return fmt.Errorf("no pattern at %d", index+1) + } + return e.setList(base, items) +} + +// listItems returns the effective rules of a bash list field: the explicit list +// when the key is set, otherwise the loader's built-in defaults. Always a copy, +// so callers may edit it. +func (e *ConfigEditor) listItems(key string) []string { + var list []string + switch key { + case keyPermBashAllow: + list = bashStringList(e.doc, func(b *project.BashDoc) *project.StringList { return b.Allow }) + case keyPermBashDeny: + list = bashStringList(e.doc, func(b *project.BashDoc) *project.StringList { return b.Deny }) + default: + return nil + } + if list == nil { + policy := permission.DefaultPolicy() + if key == keyPermBashAllow { + list = policy.BashAllow + } else { + list = policy.BashDeny + } + } + return append([]string(nil), list...) +} + +// listSet reports whether the rules are written in the file, which is when +// there is no such key. +func (e *ConfigEditor) listSet(key string) bool { + switch key { + case keyPermBashAllow: + return bashStringList(e.doc, func(b *project.BashDoc) *project.StringList { return b.Allow }) != nil + case keyPermBashDeny: + return bashStringList(e.doc, func(b *project.BashDoc) *project.StringList { return b.Deny }) != nil + default: + return false + } +} + +// setList writes rules as an explicit list. An empty list stays explicit on +// purpose: it is how a user says "no patterns at all". +func (e *ConfigEditor) setList(key string, items []string) error { + list := project.StringList(items) + b := bash(e.doc) + switch key { + case keyPermBashAllow: + b.Allow = &list + case keyPermBashDeny: + b.Deny = &list + default: + return fmt.Errorf("unknown rule list %q", key) + } + return nil +} + +// removeListItem drops one rule, materializing the built-in rules first when +// the file had none. +func (e *ConfigEditor) removeListItem(key string, index int) { + items := e.listItems(key) + if index < 0 || index >= len(items) { + return + } + items = append(items[:index:index], items[index+1:]...) + if err := e.setList(key, items); err != nil { + e.setError(err.Error()) + } +} + +func (e *ConfigEditor) modelAt(index int) *project.ModelDoc { + if index < 0 || index >= len(e.doc.Models) { + return nil + } + return &e.doc.Models[index] +} + +func modelNote(m project.ModelDoc) string { + preset, known := model.Lookup(m.Name) + api := m.API + window := 0 + if m.ContextWindow != nil { + window = *m.ContextWindow + } + if known { + if api == "" { + api = string(preset.Config.API) + } + if window == 0 { + window = preset.Config.ContextWindow + } + } + if api == "" { + api = "auto" + } + parts := []string{api} + if window > 0 { + parts = append(parts, humanTokens(window)+" context") + } + return strings.Join(parts, " · ") +} + +// maskSecret keeps the last four characters visible: enough to tell two keys +// apart while editing a list of models, not enough to leak one. +func maskSecret(value string) string { + runes := []rune(value) + if len(runes) <= 8 { + return strings.Repeat("•", len(runes)) + } + return "••••" + string(runes[len(runes)-4:]) +} + +func humanTokens(n int) string { + switch { + case n <= 0: + return "" + case n >= 1_000_000: + // A whole million stays "1M"; a fractional one keeps one digit so a + // 1.28M context is not rounded down to "1M". + if n%1_000_000 == 0 { + return strconv.Itoa(n/1_000_000) + "M" + } + return strconv.FormatFloat(float64(n)/1_000_000, 'f', 1, 64) + "M" + case n >= 1000: + return strconv.Itoa(n/1000) + "k" + default: + return strconv.Itoa(n) + } +} + +func plural(n int, noun string) string { + if n == 1 { + return "1 " + noun + } + return strconv.Itoa(n) + " " + noun + "s" +} + +func onOff(on bool) string { + if on { + return triOn + } + return triOff +} + +func triString(v *bool) string { + if v == nil { + return triAuto + } + return onOff(*v) +} + +func parseTri(value string) (*bool, error) { + switch value { + case triAuto: + return nil, nil + case triOn: + on := true + return &on, nil + case triOff: + off := false + return &off, nil + default: + return nil, fmt.Errorf("expected auto, on, or off — got %q", value) + } +} + +// cycleTri advances the tri-state toggle: auto (absent) → on → off → auto. +func cycleTri(raw string) string { + switch raw { + case triAuto: + return triOn + case triOn: + return triOff + default: + return triAuto + } +} + +func parsePositiveInt(value, unit string) (*int, error) { + value = strings.TrimSpace(value) + if value == "" { + return nil, nil + } + n, err := strconv.Atoi(value) + if err != nil { + return nil, fmt.Errorf("%q is not a number", value) + } + if n <= 0 { + return nil, fmt.Errorf("must be a positive number of %s", unit) + } + return &n, nil +} + +func optionalString(value string) *string { + if value == "" { + return nil + } + return &value +} + +func stringValue(v *string) string { + if v == nil { + return "" + } + return *v +} + +func intString(v *int) string { + if v == nil { + return "" + } + return strconv.Itoa(*v) +} + +func defaultTimeout() int { return permission.DefaultPolicy().AskTimeoutSec } + +func perms(doc *project.ConfigDoc) *project.PermDoc { + if doc.Permissions == nil { + doc.Permissions = &project.PermDoc{} + } + return doc.Permissions +} + +func bash(doc *project.ConfigDoc) *project.BashDoc { + p := perms(doc) + if p.Bash == nil { + p.Bash = &project.BashDoc{} + } + return p.Bash +} + +func agents(doc *project.ConfigDoc) *project.AgentsDoc { + if doc.Agents == nil { + doc.Agents = &project.AgentsDoc{} + } + return doc.Agents +} + +func agentsModels(doc *project.ConfigDoc) *project.AgentsRoleModels { + a := agents(doc) + if a.Models == nil { + a.Models = &project.AgentsRoleModels{} + } + return a.Models +} + +func permString(doc *project.ConfigDoc, pick func(*project.PermDoc) string) string { + if doc.Permissions != nil { + return pick(doc.Permissions) + } + return "" +} + +func permBool(doc *project.ConfigDoc, pick func(*project.PermDoc) *bool) *bool { + if doc.Permissions != nil { + return pick(doc.Permissions) + } + return nil +} + +func permInt(doc *project.ConfigDoc, pick func(*project.PermDoc) *int) *int { + if doc.Permissions != nil { + return pick(doc.Permissions) + } + return nil +} + +func bashString(doc *project.ConfigDoc, pick func(*project.BashDoc) string) string { + if doc.Permissions != nil && doc.Permissions.Bash != nil { + return pick(doc.Permissions.Bash) + } + return "" +} + +func bashStringList(doc *project.ConfigDoc, pick func(*project.BashDoc) *project.StringList) []string { + if doc.Permissions == nil || doc.Permissions.Bash == nil { + return nil + } + if list := pick(doc.Permissions.Bash); list != nil { + return *list + } + return nil +} + +func agentsBool(doc *project.ConfigDoc, pick func(*project.AgentsDoc) *bool) *bool { + if doc.Agents != nil { + return pick(doc.Agents) + } + return nil +} + +func agentsString(doc *project.ConfigDoc, pick func(*project.AgentsRoleModels) string) string { + if doc.Agents != nil && doc.Agents.Models != nil { + return pick(doc.Agents.Models) + } + return "" +} diff --git a/internal/tui/configui/render.go b/internal/tui/configui/render.go new file mode 100644 index 00000000..0a0e724c --- /dev/null +++ b/internal/tui/configui/render.go @@ -0,0 +1,380 @@ +package configui + +import ( + "os" + "strings" + + "github.com/pulseaiclub/xui" + + "github.com/pulseaiclub/phi/internal/components" + "github.com/pulseaiclub/phi/internal/components/chrome" + "github.com/pulseaiclub/phi/internal/components/layout" +) + +// Row geometry. Every column is fixed so the eye can run down one line of +// labels and one line of values. +const ( + xSection = 2 + xModel = 4 + xField = 4 + xModelField = 6 + xNested = 8 +) + +// valueCol is the shared value column; it shrinks on narrow terminals. +func (e *ConfigEditor) valueCol() int { + col := 28 + if e.width > 0 { + col = min(col, max(e.width/3, 16)) + } + return col +} + +// Draw paints the frame the App loop shows. +func (e *ConfigEditor) Draw(ctx components.DrawContext) components.Surface { + w, h := ctx.Max.Width, ctx.Max.Height + if w <= 0 { + w = 80 + } + if h <= 0 { + h = 24 + } + e.width = w + e.pollFetch() + e.rebuild() + + th := e.themeOrDefault() + out := components.NewSurface(w, h, e) + layout.DrawRoundedBorder(&out, layout.BorderRounded, th.Border, + e.topLabel(), nil, nil, nil, ctx.Method) + + inner := max(h-2, 1) + footer := 2 + if inner < 5 { + footer = 0 + } + page := max(inner-footer, 1) + e.page = page + e.rowsTop = 1 + e.ensureVisible(page) + + for i := e.scroll; i < len(e.rows) && i < e.scroll+page; i++ { + e.paintRow(&out, 1+i-e.scroll, e.rows[i], i == e.cursor, ctx.Method) + } + if footer > 0 { + e.paintFooter(&out, ctx.Method) + } + if e.edit != nil { + out.Cursor = &components.Point{X: e.caretX, Y: e.caretY} + } + + if e.confirm != nil { + out.Children = append(out.Children, e.drawConfirm(w, h, ctx.Method)) + } + if e.picker.Open { + out.Children = append(out.Children, components.SubSurface{ + Z: 20, + Surface: e.picker.Draw(ctx), + }) + } + return out +} + +// topLabel is the title bar: what this screen is, then the file it writes, +// then an unsaved marker. The title comes first so a long path is what gets +// truncated on a narrow terminal. +func (e *ConfigEditor) topLabel() *layout.BorderLabel { + th := e.themeOrDefault() + spans := []layout.BorderSpan{ + {Text: " phi config ", Style: chrome.PanelTitle(th)}, + {Text: "· " + displayPath(e.path) + " ", Style: th.Muted}, + } + if e.dirty { + spans = append(spans, layout.BorderSpan{Text: "● ", Style: th.Warning}) + } + return &layout.BorderLabel{Spans: spans} +} + +func (e *ConfigEditor) ensureVisible(page int) { + if len(e.rows) == 0 { + e.cursor, e.scroll = 0, 0 + return + } + e.cursor = min(max(e.cursor, 0), len(e.rows)-1) + if e.cursor < e.scroll { + e.scroll = e.cursor + } + if e.cursor >= e.scroll+page { + e.scroll = e.cursor - page + 1 + } + e.scroll = min(max(e.scroll, 0), max(len(e.rows)-page, 0)) +} + +func (e *ConfigEditor) paintRow(s *components.Surface, y int, r row, selected bool, method xui.WidthMethod) { + th := e.themeOrDefault() + base, muted := th.Foreground, th.Muted + bg := th.SelectionBg.Bg + if selected { + for x := 1; x < s.Size.Width-1; x++ { + s.SetCell(x, y, xui.Cell{Char: " ", Width: 1, Style: xui.Style{Bg: bg}}) + } + base = th.SelectionFg + base.Bg = bg + muted = base + muted.Dim = false + } + switch r.kind { + case rowSection: + e.paintSection(s, y, r, method) + case rowModel: + name := base + name.Bold = true + x := xModel + x += s.Print(x, y, r.name, name, method) + if r.badge != "" { + s.Print(x+1, y, r.badge, withBG(th.Success, bg, selected), method) + } + if r.note != "" { + col := e.valueCol() + s.Print(col, y, layout.TruncateToWidth(r.note, s.Size.Width-2-col, method), muted, method) + } + case rowItem: + x := xNested + x += s.Print(x, y, "· ", th.Border, method) + style := e.valStyle(r.style, bg, selected) + text := r.label + if e.edit != nil && e.edit.key == r.key { + text = string(e.edit.buf) + style = base + if selected { + style.Bg = bg + } + } + s.Print(x, y, layout.TruncateToWidth(text, s.Size.Width-2-x, method), style, method) + if e.edit != nil && e.edit.key == r.key { + e.caretX = x + xui.StringWidth(string(e.edit.buf[:e.edit.cur]), method) + e.caretY = y + } + case rowAdd: + style := th.ToolName + if selected { + style = base + } + s.Print(xNested, y, layout.TruncateToWidth(r.label, s.Size.Width-2-xNested, method), style, method) + case rowField: + e.paintField(s, y, r, selected, base, muted, bg, method) + } +} + +func (e *ConfigEditor) paintField( + s *components.Surface, + y int, + r row, + selected bool, + base, muted xui.Style, + bg xui.Color, + method xui.WidthMethod, +) { + th := e.themeOrDefault() + x := xField + if _, _, ok := splitModelKey(r.key); ok { + x = xModelField + } + col := e.valueCol() + labelStyle := muted + if selected { + labelStyle = base + } + s.Print(x, y, layout.TruncateToWidth(r.label, max(col-x-2, 1), method), labelStyle, method) + + style := e.valStyle(r.style, bg, selected) + text := r.display + editing := e.edit != nil && e.edit.key == r.key + if editing { + text = string(e.edit.buf) + style = base + if selected { + style.Bg = bg + } + } + avail := max(s.Size.Width-2-col, 1) + printed := s.Print(col, y, layout.TruncateToWidth(text, avail, method), style, method) + if editing { + e.caretX = col + xui.StringWidth(string(e.edit.buf[:e.edit.cur]), method) + e.caretY = y + return + } + if selected && r.hint != "" { + note := th.Muted + note.Dim = true + s.Print(col+printed+2, y, layout.TruncateToWidth(r.hint, max(avail-printed-2, 0), method), note, method) + } +} + +func (e *ConfigEditor) paintSection(s *components.Surface, y int, r row, method xui.WidthMethod) { + th := e.themeOrDefault() + title := th.TitleOrForeground() + x := xSection + x += s.Print(x, y, strings.ToUpper(r.title), title, method) + + end := s.Size.Width - 2 + note := r.note + noteW := 0 + if note != "" { + noteW = xui.StringWidth(note, method) + 1 + // Drop the note rather than let it crowd out the rule on a narrow frame. + if end-x-1-noteW < 2 { + note, noteW = "", 0 + } + } + if rule := end - x - 1 - noteW; rule > 0 { + s.Print(x+1, y, strings.Repeat("─", rule), th.Border, method) + } + if note != "" { + s.Print(end-noteW+1, y, note, th.Muted, method) + } +} + +func (e *ConfigEditor) paintFooter(s *components.Surface, method xui.WidthMethod) { + th := e.themeOrDefault() + w, h := s.Size.Width, s.Size.Height + ruleY := h - 3 + if ruleY > 1 { + s.Print(1, ruleY, strings.Repeat("─", max(w-2, 0)), th.Border, method) + } + y := h - 2 + hints := e.hintLine() + printed := s.Print(2, y, layout.TruncateToWidth(hints, max(w-6, 1), method), th.Muted, method) + if e.status == "" { + return + } + statusW := xui.StringWidth(e.status, method) + start := w - 2 - statusW + if start <= 4+printed { + return + } + s.Print(start, y, layout.TruncateToWidth(e.status, max(w-4, 1), method), e.statusStyle(), method) +} + +func (e *ConfigEditor) hintLine() string { + switch { + case e.edit != nil: + return "⏎ apply" + chrome.Sep + "esc cancel" + chrome.Sep + "^u clear" + case e.confirm != nil: + return chrome.ConfirmHint() + case e.expanded != "": + return "↑↓ move" + chrome.Sep + "⏎ edit" + chrome.Sep + "a add" + chrome.Sep + "d delete" + chrome.Sep + "esc close" + default: + return "↑↓ move" + chrome.Sep + "⏎ edit" + chrome.Sep + "←→ cycle" + chrome.Sep + + "a add model" + chrome.Sep + "f fetch" + chrome.Sep + "s save" + chrome.Sep + "q quit" + } +} + +func (e *ConfigEditor) statusStyle() xui.Style { + th := e.themeOrDefault() + switch e.statusKind { + case statusError: + return th.Destructive + case statusSuccess: + return th.Success + default: + return th.Muted + } +} + +func (e *ConfigEditor) valStyle(v valueStyle, bg xui.Color, selected bool) xui.Style { + th := e.themeOrDefault() + var st xui.Style + switch v { + case styleMuted: + st = th.Muted + case styleOn: + st = th.Success + case styleWarn: + st = th.Warning + default: + st = th.Foreground + } + if selected { + st.Bg = bg + st.Dim = false + } + return st +} + +type statusKind int + +const ( + statusNone statusKind = iota + statusInfo + statusSuccess + statusError +) + +func withBG(st xui.Style, bg xui.Color, selected bool) xui.Style { + if selected { + st.Bg = bg + } + return st +} + +// drawConfirm paints the yes/no modal centered over the form. +func (e *ConfigEditor) drawConfirm(w, h int, method xui.WidthMethod) components.SubSurface { + th := e.themeOrDefault() + st := e.confirm + + width := min(58, max(w-4, 20)) + bodyW := width - 4 + var lines []components.RichLine + add := func(spans ...components.Span) { + lines = append(lines, components.WrapSpans(spans, bodyW, method)...) + } + add(components.Span{Text: st.title, Style: th.TitleOrForeground()}) + if st.body != "" { + add(components.Span{Text: st.body, Style: th.Muted}) + } + lines = append(lines, components.RichLine{}) + + primary := chrome.DecisionPrimary(th) + border := chrome.ModalBorder(th) + if st.danger { + border = th.Destructive + } + for i, label := range []string{"Yes", "No"} { + lines = append(lines, chrome.OptionLine(th, primary, label, (i == 0) == st.yes, bodyW, method)...) + } + add(components.Span{Text: chrome.ConfirmHint(), Style: th.Muted}) + + height := len(lines) + 2 + panel := components.NewSurface(width, height, e) + fill := xui.Style{Fg: th.Foreground.Fg} + for y := range height { + for x := range width { + panel.SetCell(x, y, xui.Cell{Char: " ", Width: 1, Style: fill}) + } + } + layout.DrawRoundedBorder(&panel, layout.BorderRounded, border, nil, nil, nil, nil, method) + y := 1 + for _, line := range lines { + if y >= height-1 { + break + } + components.PaintSpans(&panel, 2, y, line, method) + y++ + } + return components.SubSurface{ + Origin: components.Point{X: max((w-width)/2, 0), Y: max((h-height)/2, 0)}, + Z: 30, + Surface: panel, + } +} + +// displayPath shortens the home directory to "~" for the border label. +func displayPath(path string) string { + if home, err := os.UserHomeDir(); err == nil && home != "" { + if rest, ok := strings.CutPrefix(path, home); ok { + return "~" + rest + } + } + return path +} From 03c07f70c35eecbae0592599b86cf15c6044e768 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Sep 2026 17:14:43 +0800 Subject: [PATCH 2/4] configui: give the form's two columns one job each Labels keep a flat quiet tone (Muted, no Dim) so the left column never reads as mud. Values are the only column that carries emphasis: body text when set, the same quiet tone when absent, and semantic color only for state (on, not set, default). A filled api_key now reads as data instead of looking empty. The selection stripe also stops punching holes: the trailing hint and list bullets now carry the stripe background. An unset field stays quiet under the cursor so it can never be mistaken for a filled one. Tests lock the grammar: label vs value vs absent tones, and the stripe's background coverage. --- CHANGELOG.md | 15 ++ internal/components/chrome/chrome.go | 5 +- internal/components/chrome/chrome_test.go | 2 +- internal/tui/configui/editor.go | 13 +- internal/tui/configui/editor_test.go | 171 ++++++++++++++++++++++ internal/tui/configui/model.go | 5 +- internal/tui/configui/render.go | 65 +++++--- 7 files changed, 249 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9862ad..3d44feae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm `ReadConfigDoc`, `Save`, `Validate`). The runtime parser and the editor now share one definition instead of two hand-kept copies, and empty `api_key` / `base_url` values are omitted when a file is written. +- The `phi config` form gives its two columns one job each: labels and values the + file does not set share a single quiet tone, real values are body text, and + state (`on`, `default`, `not set`) is the only thing that spends a color. The + label column no longer relies on `Dim`, which terminals render anywhere from + soft to invisible, and a filled `api_key` now reads as data instead of as an + empty field. ### Deprecated @@ -42,6 +48,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm file-operation lists are refreshed once instead of accumulating duplicate blocks. - Picker hints (`/` commands, `@` files, `?` shortcuts) now start in one shared column instead of trailing each label's own length. +- `phi config` model headers now sit on the same grid as every other row: the + model name owns the label column (truncated when it does not fit) and the + `default` badge plus the api/context summary share the value column. A long + name used to reach into the summary and eat the badge's last letter. + +- The `phi config` confirm modal read its arrows backwards: `↑`/`←` selected No + and `↓`/`→` selected Yes, so pressing a key in the visible direction looked + dead. The keys now pick the row above / below like every other decision + panel, and the footer says `↑↓ move` instead of pointing at a horizontal axis. ### Security diff --git a/internal/components/chrome/chrome.go b/internal/components/chrome/chrome.go index b070af82..3efefde3 100644 --- a/internal/components/chrome/chrome.go +++ b/internal/components/chrome/chrome.go @@ -74,9 +74,10 @@ func AskHint(nav, action, dismiss string) string { return strings.Join(parts, Sep) } -// ConfirmHint is the confirm-panel footer (includes Y/N chords). +// ConfirmHint is the confirm-panel footer (includes Y/N chords). Decision rows +// stack vertically, so the nav fragment names the vertical axis. func ConfirmHint() string { - return "←→ move" + Sep + "Enter confirm" + Sep + "Y yes" + Sep + "N/Esc cancel" + return "↑↓ move" + Sep + "Enter confirm" + Sep + "Y yes" + Sep + "N/Esc cancel" } // FeedbackHint is the deny-with-feedback footer. diff --git a/internal/components/chrome/chrome_test.go b/internal/components/chrome/chrome_test.go index fbdb1e9e..9bf0251c 100644 --- a/internal/components/chrome/chrome_test.go +++ b/internal/components/chrome/chrome_test.go @@ -17,7 +17,7 @@ func TestHintDialect(t *testing.T) { assert.Equal(t, " ⏎ select · esc close ", chrome.ListHintShort("select")) assert.Equal(t, "↑↓ move · Enter select · Esc cancel", chrome.AskHint("↑↓ move", "select", "cancel")) assert.Equal(t, "↑↓ move · Enter select · Esc stop", chrome.AskHint("↑↓ move", "select", "stop")) - assert.Equal(t, "←→ move · Enter confirm · Y yes · N/Esc cancel", chrome.ConfirmHint()) + assert.Equal(t, "↑↓ move · Enter confirm · Y yes · N/Esc cancel", chrome.ConfirmHint()) assert.Equal(t, "Enter send · Esc cancel", chrome.FeedbackHint()) } diff --git a/internal/tui/configui/editor.go b/internal/tui/configui/editor.go index a2ba14fc..e5c6eda7 100644 --- a/internal/tui/configui/editor.go +++ b/internal/tui/configui/editor.go @@ -500,13 +500,16 @@ func (e *ConfigEditor) requestQuit(ctx *components.EventContext) { } } +// handleConfirmKey moves between the two decision rows the way every other +// panel does: up/left picks the row above, down/right/tab the row below. The +// rows are painted Yes then No, so yes is the first one. func (e *ConfigEditor) handleConfirmKey(ctx *components.EventContext, k xui.KeyEvent) { st := e.confirm switch k.Code { - case xui.KeyLeft, xui.KeyUp: - st.yes = false - case xui.KeyRight, xui.KeyDown, xui.KeyTab: + case xui.KeyUp, xui.KeyLeft: st.yes = true + case xui.KeyDown, xui.KeyRight, xui.KeyTab: + st.yes = false case xui.KeyEnter: e.confirm = nil if st.yes && st.run != nil { @@ -524,9 +527,9 @@ func (e *ConfigEditor) handleConfirmKey(ctx *components.EventContext, k xui.KeyE case 'n', 'N': e.confirm = nil case 'h', 'H', 'k', 'K': - st.yes = false - case 'l', 'L', 'j', 'J': st.yes = true + case 'l', 'L', 'j', 'J': + st.yes = false } default: ctx.Consume = true diff --git a/internal/tui/configui/editor_test.go b/internal/tui/configui/editor_test.go index 8df96f1f..8f0c55bf 100644 --- a/internal/tui/configui/editor_test.go +++ b/internal/tui/configui/editor_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "time" @@ -202,6 +203,39 @@ func TestAddAndDeleteModel(t *testing.T) { assert.True(t, doc.Models[0].Default) } +// The confirm rows are painted Yes then No, so the keys must follow that order: +// up/left to the row above, down/right/tab to the row below. Reversed keys made +// the highlight jump at the same key the user pressed and stand still on a +// second press, which read as an unresponsive dialog. +func TestConfirmArrowsFollowRowOrder(t *testing.T) { + doc := sampleDoc() + e := newTestEditor(t, doc) + + focus(t, e, modelKey(0, fName)) + typeRune(e, 'd') + require.NotNil(t, e.confirm) + require.False(t, e.confirm.yes, "a destructive confirm starts on No, the second row") + + press(e, xui.KeyUp, 0) + assert.True(t, e.confirm.yes, "↑ selects the first row (Yes)") + press(e, xui.KeyDown, 0) + assert.False(t, e.confirm.yes, "↓ selects the second row (No)") + + press(e, xui.KeyLeft, 0) + assert.True(t, e.confirm.yes, "← is the same axis as ↑") + press(e, xui.KeyRight, 0) + assert.False(t, e.confirm.yes, "→ is the same axis as ↓") + + press(e, xui.KeyRune, 'k') + assert.True(t, e.confirm.yes) + press(e, xui.KeyRune, 'j') + assert.False(t, e.confirm.yes) + + press(e, xui.KeyEscape, 0) + assert.Nil(t, e.confirm) + assert.Len(t, doc.Models, 2, "a cancel must not delete the model") +} + func TestDeleteModelCanBeCancelled(t *testing.T) { doc := sampleDoc() e := newTestEditor(t, doc) @@ -420,6 +454,43 @@ func TestDrawPaintsFormAndStatus(t *testing.T) { assert.Contains(t, components.SurfaceText(surf), "●") } +// modelHeader returns the painted header line of one model, without the right +// border or trailing padding. Header rows start at xModel, field rows at +// xModelField, so the leading cell fixes which one this is. +func modelHeader(t *testing.T, text, name string) string { + t.Helper() + for l := range strings.SplitSeq(text, "\n") { + if strings.HasPrefix(l, "│ "+name) { + return strings.TrimRight(l, " │") + } + } + require.FailNow(t, "no model header painted", "name=%q", name) + return "" +} + +func TestDrawAlignsModelHeadersToValueColumn(t *testing.T) { + window := 1_000_000 + doc := &project.ConfigDoc{Models: []project.ModelDoc{ + {Name: "deepseek-v4-flash", API: "OpenAIResponses", ContextWindow: &window, Default: true}, + {Name: "glm-5.2", API: "OpenAIResponses", ContextWindow: &window}, + }} + e := newTestEditor(t, doc) + + surf := e.Draw(components.DrawContext{Max: components.Size{Width: 100, Height: 48}, Method: xui.WidthUnicode}) + col := e.valueCol() + text := components.SurfaceText(surf) + + // The name owns the label column, and the default marker plus the summary + // share the value column: a long name is truncated rather than allowed to + // paint over the badge. + name := "deepseek-v4-flash" + pad := strings.Repeat(" ", col-xModel-len(name)) + assert.Equal(t, "│ "+name+pad+"default · OpenAIResponses · 1M context", modelHeader(t, text, name)) + assert.Equal(t, + "│ glm-5.2"+strings.Repeat(" ", col-xModel-len("glm-5.2"))+"OpenAIResponses · 1M context", + modelHeader(t, text, "glm-5.2")) +} + func TestDrawIsStableOnTinyScreens(t *testing.T) { e := newTestEditor(t, sampleDoc()) for _, size := range []components.Size{{Width: 1, Height: 1}, {Width: 20, Height: 4}, {Width: 40, Height: 6}} { @@ -543,6 +614,106 @@ func TestFetchDiscardedWhenModelMoves(t *testing.T) { assert.Contains(t, e.status, "changed while fetching") } +// drawForm paints the editor at a fixed frame size and returns the surface. +func drawForm(t *testing.T, e *ConfigEditor) components.Surface { + t.Helper() + const width, height = 100, 40 + return e.Draw(components.DrawContext{ + Max: components.Size{Width: width, Height: height}, + Method: xui.WidthUnicode, + }) +} + +// formRowY is the screen row a form key landed on in the last Draw. +func formRowY(t *testing.T, e *ConfigEditor, key string) int { + t.Helper() + for i := range e.rows { + if e.rows[i].key == key { + return 1 + i - e.scroll + } + } + t.Fatalf("no row with key %q", key) + return 0 +} + +func paintedStyle(t *testing.T, out components.Surface, x, y int) xui.Style { + t.Helper() + require.Less(t, x, out.Size.Width) + require.Less(t, y, out.Size.Height) + return out.Buffer[y*out.Size.Width+x].Style +} + +// TestFormColumnsKeepOneQuietTone locks the two-column color grammar: the label +// column is one quiet tone, the value column carries the data, and a value the +// file does not set drops back to the label tone instead of inventing a third +// shade of gray. +func TestFormColumnsKeepOneQuietTone(t *testing.T) { + // Only the name and the masked api key are set; every other field shows + // what the loader would supply instead. + doc := &project.ConfigDoc{Models: []project.ModelDoc{{Name: "model-a", APIKey: "key-a"}}} + e := newTestEditor(t, doc) + th := components.DarkTheme() + require.NotEqual(t, th.Muted.Fg, th.Foreground.Fg, "fixture needs a theme with two distinct tones") + // Park the cursor on the last row so the asserted rows are not selected. + focus(t, e, modelKey(0, fDeflt)) + out := drawForm(t, e) + col := e.valueCol() + quiet := secondaryStyle(th) + + nameY := formRowY(t, e, modelKey(0, fName)) + label := paintedStyle(t, out, xModelField, nameY) + assert.Equal(t, quiet.Fg, label.Fg, "the label column keeps the quiet tone") + assert.False(t, label.Dim, "Dim ranges from soft to invisible; labels must not rely on it") + assert.Equal(t, th.Foreground.Fg, paintedStyle(t, out, col, nameY).Fg, "a set value is body text") + + keyY := formRowY(t, e, modelKey(0, fKey)) + assert.Equal(t, th.Foreground.Fg, paintedStyle(t, out, col, keyY).Fg, + "a masked key is set, so it reads as data rather than as an empty field") + + // base_url and context_window are absent from the document. + for _, key := range []string{modelKey(0, fURL), modelKey(0, fCtx)} { + assert.Equal(t, quiet.Fg, paintedStyle(t, out, col, formRowY(t, e, key)).Fg, + "%s must not look set", key) + } +} + +// TestSelectedRowIsOneStripe pins what a cursor used to do to the two columns: a +// hole in the stripe behind a trailing hint, "not set" promoted into looking +// exactly like a value, and emphasis moved onto the label column. +func TestSelectedRowIsOneStripe(t *testing.T) { + doc := &project.ConfigDoc{Models: []project.ModelDoc{{Name: "model-a"}}} + e := newTestEditor(t, doc) + th := components.DarkTheme() + + // base_url is unset, so the row paints a placeholder plus a trailing hint. + key := modelKey(0, fURL) + focus(t, e, key) + out := drawForm(t, e) + y := formRowY(t, e, key) + + for x := 1; x < out.Size.Width-1; x++ { + assert.Equal(t, th.SelectionBg.Bg, paintedStyle(t, out, x, y).Bg, + "cell %d of the selected row must keep the stripe background", x) + } + + label := paintedStyle(t, out, xModelField, y) + empty := paintedStyle(t, out, e.valueCol(), y) + assert.Equal(t, th.SelectionFg.Fg, label.Fg, "the selected label stays readable") + assert.False(t, label.Bold, "the label never outweighs the value") + assert.Equal(t, secondaryStyle(th).Fg, empty.Fg, "a selected empty field still reads as empty") + assert.False(t, empty.Dim, "the quiet tone stays flat, on the stripe too") + + // A filled row puts the emphasis on the value, not on the label. + focus(t, e, modelKey(0, fName)) + out = drawForm(t, e) + y = formRowY(t, e, modelKey(0, fName)) + filledLabel := paintedStyle(t, out, xModelField, y) + filled := paintedStyle(t, out, e.valueCol(), y) + assert.Equal(t, th.SelectionFg.Fg, filled.Fg) + assert.True(t, filled.Bold, "the value column carries the emphasis") + assert.False(t, filledLabel.Bold) +} + func TestHumanTokens(t *testing.T) { cases := map[int]string{ 0: "", diff --git a/internal/tui/configui/model.go b/internal/tui/configui/model.go index 385c0cff..45b93e91 100644 --- a/internal/tui/configui/model.go +++ b/internal/tui/configui/model.go @@ -380,7 +380,10 @@ func (e *ConfigEditor) fieldRow(sp fieldSpec) row { if raw == "" { r.display, r.style = "not set", styleWarn } else { - r.display, r.style = maskSecret(raw), styleMuted + // A key that is set is data like any other value: bright dots say + // "set", while the quiet tone is reserved for absent or inherited + // values. + r.display, r.style = maskSecret(raw), styleValue } case kindInt: r.display, r.style = raw, styleValue diff --git a/internal/tui/configui/render.go b/internal/tui/configui/render.go index 0a0e724c..dcea94c8 100644 --- a/internal/tui/configui/render.go +++ b/internal/tui/configui/render.go @@ -112,7 +112,7 @@ func (e *ConfigEditor) ensureVisible(page int) { func (e *ConfigEditor) paintRow(s *components.Surface, y int, r row, selected bool, method xui.WidthMethod) { th := e.themeOrDefault() - base, muted := th.Foreground, th.Muted + base, secondary := th.Foreground, secondaryStyle(th) bg := th.SelectionBg.Bg if selected { for x := 1; x < s.Size.Width-1; x++ { @@ -120,27 +120,36 @@ func (e *ConfigEditor) paintRow(s *components.Surface, y int, r row, selected bo } base = th.SelectionFg base.Bg = bg - muted = base - muted.Dim = false + // Text on the stripe takes the stripe's tone, but the label column stays + // one weight below the value column: the eye belongs on the data, not on + // the key names. + secondary = base + secondary.Bold = false } switch r.kind { case rowSection: e.paintSection(s, y, r, method) case rowModel: - name := base - name.Bold = true - x := xModel - x += s.Print(x, y, r.name, name, method) + // A model header is a label/value row like any other: the name owns the + // label column and the default marker plus the preset summary share the + // value column, so a long name can never overwrite the badge. + col := e.valueCol() + nameStyle := base + nameStyle.Bold = true + s.Print(xModel, y, layout.TruncateToWidth(r.name, max(col-1-xModel, 1), method), nameStyle, method) + x := col if r.badge != "" { - s.Print(x+1, y, r.badge, withBG(th.Success, bg, selected), method) + x += s.Print(x, y, r.badge, withBG(th.Success, bg, selected), method) + if r.note != "" { + x += s.Print(x, y, chrome.Sep, secondary, method) + } } if r.note != "" { - col := e.valueCol() - s.Print(col, y, layout.TruncateToWidth(r.note, s.Size.Width-2-col, method), muted, method) + s.Print(x, y, layout.TruncateToWidth(r.note, max(s.Size.Width-2-x, 1), method), secondary, method) } case rowItem: x := xNested - x += s.Print(x, y, "· ", th.Border, method) + x += s.Print(x, y, "· ", withBG(th.Border, bg, selected), method) style := e.valStyle(r.style, bg, selected) text := r.label if e.edit != nil && e.edit.key == r.key { @@ -162,7 +171,7 @@ func (e *ConfigEditor) paintRow(s *components.Surface, y int, r row, selected bo } s.Print(xNested, y, layout.TruncateToWidth(r.label, s.Size.Width-2-xNested, method), style, method) case rowField: - e.paintField(s, y, r, selected, base, muted, bg, method) + e.paintField(s, y, r, selected, base, secondary, bg, method) } } @@ -171,7 +180,7 @@ func (e *ConfigEditor) paintField( y int, r row, selected bool, - base, muted xui.Style, + base, secondary xui.Style, bg xui.Color, method xui.WidthMethod, ) { @@ -181,10 +190,9 @@ func (e *ConfigEditor) paintField( x = xModelField } col := e.valueCol() - labelStyle := muted - if selected { - labelStyle = base - } + // Labels are a reading aid: they never take emphasis away from the value + // column, on or off the stripe. + labelStyle := secondary s.Print(x, y, layout.TruncateToWidth(r.label, max(col-x-2, 1), method), labelStyle, method) style := e.valStyle(r.style, bg, selected) @@ -207,6 +215,8 @@ func (e *ConfigEditor) paintField( if selected && r.hint != "" { note := th.Muted note.Dim = true + // The selection bar is one continuous stripe, hint included. + note.Bg = bg s.Print(col+printed+2, y, layout.TruncateToWidth(r.hint, max(avail-printed-2, 0), method), note, method) } } @@ -282,12 +292,25 @@ func (e *ConfigEditor) statusStyle() xui.Style { } } +// secondaryStyle is the form's one quiet tone: field labels, values the file +// does not set, and rules inherited from the built-in preset all share it. +// +// Dim is cleared on purpose. SGR 2 lands anywhere between "slightly softer" and +// "all but invisible" depending on the terminal, and a label column that sinks +// into the background next to a crisp value column is what makes a two-column +// form read as mud. Trailing hints keep Dim — they are meant to disappear first. +func secondaryStyle(th components.Theme) xui.Style { + st := th.Muted + st.Dim = false + return st +} + func (e *ConfigEditor) valStyle(v valueStyle, bg xui.Color, selected bool) xui.Style { th := e.themeOrDefault() var st xui.Style switch v { case styleMuted: - st = th.Muted + st = secondaryStyle(th) case styleOn: st = th.Success case styleWarn: @@ -296,6 +319,12 @@ func (e *ConfigEditor) valStyle(v valueStyle, bg xui.Color, selected bool) xui.S st = th.Foreground } if selected { + // A filled value takes the stripe highlight; an unset one keeps the flat + // quiet tone it has everywhere else, so the cursor cannot make an empty + // field look written. + if v == styleValue { + st = th.SelectionFg + } st.Bg = bg st.Dim = false } From 55126e784f5a6e1a4bdca77949f953abc7af2476 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Sep 2026 17:22:57 +0800 Subject: [PATCH 3/4] docs: add phi config viewer screenshot to README --- README.md | 2 ++ assets/phi-config.png | Bin 0 -> 125843 bytes 2 files changed, 2 insertions(+) create mode 100644 assets/phi-config.png diff --git a/README.md b/README.md index ead257b3..9c60c711 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ A lean, high-performance terminal coding agent harness in Go — a sibling to Pi ![phi diff review](assets/diff.png) +![phi config viewer](assets/phi-config.png) + - [Docs](https://pulseaiclub.github.io/docs/getting-started/) - [Quick start](#quick-start) - [Footprint](#footprint) diff --git a/assets/phi-config.png b/assets/phi-config.png new file mode 100644 index 0000000000000000000000000000000000000000..2beeab4356883f57f3dec4552a317191649ed0f6 GIT binary patch literal 125843 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
S1*Fec+j(JwG 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

Qr5!S!K>GZX5tK|G08o#oa*ZyTAk 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 OA8By~u~JdPcmEHXqQ_PM literal 0 HcmV?d00001 From ecf2dd9fc337c14fad2ff25c92b6f38a49ad6bcb Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Sep 2026 17:46:31 +0800 Subject: [PATCH 4/4] fix(configui): normalize slashes in displayPath for windows displayPath returned the raw path tail after stripping home, so on Windows the border label and the skills fallback rendered as "~\.phi\config.yaml". Normalize both sides with filepath.ToSlash before cutting the prefix (a no-op on Unix). --- internal/tui/configui/render.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/tui/configui/render.go b/internal/tui/configui/render.go index dcea94c8..e62e3263 100644 --- a/internal/tui/configui/render.go +++ b/internal/tui/configui/render.go @@ -2,6 +2,7 @@ package configui import ( "os" + "path/filepath" "strings" "github.com/pulseaiclub/xui" @@ -398,12 +399,14 @@ func (e *ConfigEditor) drawConfirm(w, h int, method xui.WidthMethod) components. } } -// displayPath shortens the home directory to "~" for the border label. +// displayPath shortens the home directory to "~" for the border label. Slashes +// are normalized so Windows shows "~/.phi/config.yaml" instead of "~\.phi\config.yaml". func displayPath(path string) string { + display := filepath.ToSlash(path) if home, err := os.UserHomeDir(); err == nil && home != "" { - if rest, ok := strings.CutPrefix(path, home); ok { + if rest, ok := strings.CutPrefix(display, filepath.ToSlash(home)); ok { return "~" + rest } } - return path + return display }