mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat: incremental project config writes (surgical section replacement)
SaveTo now writes only delta sections/fields for project-level reasonix.toml, preserving existing file content. User configs (full template) unchanged. - RenderTOMLProjectDelta: generates only non-default sections/fields - replaceTOMLSection: handles both [section] and [[section]] formats - saveProjectIncremental merges delta into existing file
This commit is contained in:
1 parent
9ada141766
commit
2419ca27ea
3 files changed
+548
-2
No files matched your search
@@ -960,7 +960,7 @@ func rememberPermissionRule(workspaceRoot, rule string) control.RememberResult {
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
if err := edit.SaveTo(path); err != nil {
|
||||
if err := config.WritePermissionsSection(path, edit.Permissions.Allow); err != nil {
|
||||
slog.Warn("save config after permission rule", "err", err)
|
||||
result.Err = err
|
||||
return result
|
||||
|
||||
+203
-1
@@ -779,8 +779,17 @@ func validatePlugin(e PluginEntry) error {
|
||||
// writes a sibling temp file then renames, so a crash mid-write can't leave a
|
||||
// half-written reasonix.toml that fails to parse on next load. Parent directories
|
||||
// are created as needed.
|
||||
//
|
||||
// For project configs (./reasonix.toml) the write is incremental: only sections
|
||||
// and fields that differ from built-in defaults are written, so the file never
|
||||
// accumulates fields that override the user's global config. User configs still
|
||||
// write the full annotated template since they are the user's own settings store.
|
||||
func (c *Config) SaveTo(path string) error {
|
||||
return c.SaveToScope(path, renderScopeForPath(path))
|
||||
scope := renderScopeForPath(path)
|
||||
if scope == RenderScopeProject {
|
||||
return c.saveProjectIncremental(path)
|
||||
}
|
||||
return c.SaveToScope(path, scope)
|
||||
}
|
||||
|
||||
func (c *Config) SaveToScope(path string, scope RenderScope) error {
|
||||
@@ -790,6 +799,94 @@ func (c *Config) SaveToScope(path string, scope RenderScope) error {
|
||||
return writeConfigFile(path, RenderTOMLForScope(c, scope))
|
||||
}
|
||||
|
||||
// saveProjectIncremental merges only the delta (non-default sections/fields)
|
||||
// into the existing project config file, preserving all other content verbatim.
|
||||
func (c *Config) saveProjectIncremental(path string) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return fmt.Errorf("save: empty config path")
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
raw = nil
|
||||
}
|
||||
|
||||
body := string(raw)
|
||||
isNew := body == ""
|
||||
|
||||
if isNew {
|
||||
return writeConfigFile(path, RenderTOMLForScope(c, RenderScopeProject))
|
||||
}
|
||||
|
||||
delta := RenderTOMLProjectDelta(c)
|
||||
if strings.TrimSpace(delta) == "" {
|
||||
return nil // no changes to write
|
||||
}
|
||||
|
||||
// Parse delta into section blocks and merge each into body
|
||||
body = mergeTOMLDelta(body, delta)
|
||||
return writeConfigFile(path, body)
|
||||
}
|
||||
|
||||
// mergeTOMLDelta parses delta into named TOML blocks and merges each into body
|
||||
// via replaceTOMLSection. Consecutive array-of-tables entries ([[plugins]],
|
||||
// [[providers]]) with the same name are merged into a single block so the
|
||||
// replacement doesn't lose entries.
|
||||
func mergeTOMLDelta(body, delta string) string {
|
||||
lines := strings.Split(delta, "\n")
|
||||
type section struct {
|
||||
name string
|
||||
content string
|
||||
isArray bool
|
||||
}
|
||||
var sections []section
|
||||
var curName string
|
||||
var curBuf strings.Builder
|
||||
curIsArray := false
|
||||
|
||||
flush := func() {
|
||||
if curName == "" {
|
||||
return
|
||||
}
|
||||
content := curBuf.String()
|
||||
if curIsArray && len(sections) > 0 && sections[len(sections)-1].isArray && sections[len(sections)-1].name == curName {
|
||||
sections[len(sections)-1].content += content
|
||||
} else {
|
||||
sections = append(sections, section{curName, content, curIsArray})
|
||||
}
|
||||
curBuf.Reset()
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "[[") && strings.HasSuffix(trimmed, "]]"):
|
||||
flush()
|
||||
curName = trimmed[2 : len(trimmed)-2]
|
||||
curIsArray = true
|
||||
curBuf.WriteString(line + "\n")
|
||||
case strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") && !strings.HasPrefix(trimmed, "[["):
|
||||
flush()
|
||||
curName = trimmed[1 : len(trimmed)-1]
|
||||
curIsArray = false
|
||||
curBuf.WriteString(line + "\n")
|
||||
default:
|
||||
if curName != "" {
|
||||
curBuf.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
|
||||
for _, s := range sections {
|
||||
body = replaceTOMLSection(body, s.name, s.content)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// SaveMinimalProjectReasoningLanguage writes a new project config that only
|
||||
// overrides [agent].reasoning_language.
|
||||
func SaveMinimalProjectReasoningLanguage(path, lang string) (string, error) {
|
||||
@@ -820,6 +917,111 @@ func configFilePerm(path string) os.FileMode {
|
||||
return 0o644
|
||||
}
|
||||
|
||||
// WritePermissionsSection replaces or creates the [permissions] section in a
|
||||
// TOML file, preserving all other sections verbatim. When the file doesn't
|
||||
// exist yet, it creates one containing only the permissions section.
|
||||
func WritePermissionsSection(path string, allow []string) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return fmt.Errorf("write permissions: empty config path")
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
raw = nil
|
||||
}
|
||||
|
||||
newBlock := fmt.Sprintf("[permissions]\nallow = %s\n", renderStringArray(allow))
|
||||
|
||||
body := string(raw)
|
||||
if body == "" {
|
||||
return writeConfigFile(path, newBlock)
|
||||
}
|
||||
|
||||
body = replaceTOMLSection(body, "permissions", newBlock)
|
||||
return writeConfigFile(path, body)
|
||||
}
|
||||
|
||||
// replaceTOMLSection replaces the content of a named TOML section (including
|
||||
// its header line) with newContent. It handles both [section] and [[section]]
|
||||
// array-of-tables headers. If the section doesn't exist, newContent is appended
|
||||
// at the end.
|
||||
func replaceTOMLSection(body, sectionName, newContent string) string {
|
||||
headerBare := "[" + sectionName + "]"
|
||||
headerArr := "[[" + sectionName + "]]"
|
||||
|
||||
sectionStart := strings.Index(body, headerArr)
|
||||
var header string
|
||||
var headerLen int
|
||||
if sectionStart >= 0 {
|
||||
header = headerArr
|
||||
headerLen = len(headerArr)
|
||||
} else {
|
||||
sectionStart = strings.Index(body, headerBare)
|
||||
if sectionStart < 0 {
|
||||
return strings.TrimRight(body, "\n") + "\n\n" + newContent
|
||||
}
|
||||
header = headerBare
|
||||
headerLen = len(headerBare)
|
||||
}
|
||||
|
||||
// For [[array-of-tables]], find the LAST consecutive entry of the same type
|
||||
// and replace everything from the first entry through the last.
|
||||
if strings.HasPrefix(header, "[[") {
|
||||
start := sectionStart
|
||||
// Find first preceding entry of the same array
|
||||
for {
|
||||
prev := strings.LastIndex(body[:start], "\n"+headerArr)
|
||||
if prev < 0 {
|
||||
prev = strings.LastIndex(body[:start], headerArr)
|
||||
}
|
||||
if prev < 0 {
|
||||
break
|
||||
}
|
||||
// Check it's on its own line (preceded by newline or at start)
|
||||
if prev == 0 || body[prev-1] == '\n' {
|
||||
start = prev
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
sectionStart = start
|
||||
|
||||
// Find the last consecutive entry and then the next section after it
|
||||
scan := body[sectionStart:]
|
||||
lastEntryEnd := 0
|
||||
for {
|
||||
idx := strings.Index(scan, "\n"+headerArr)
|
||||
if idx < 0 {
|
||||
break
|
||||
}
|
||||
lastEntryEnd += idx + 1 + len(headerArr)
|
||||
scan = scan[idx+1+len(headerArr):]
|
||||
}
|
||||
|
||||
rest := scan
|
||||
nextSection := strings.Index(rest, "\n[")
|
||||
if nextSection >= 0 {
|
||||
sectionEnd := sectionStart + len(body) - len(scan) + nextSection
|
||||
return body[:sectionStart] + strings.TrimRight(newContent, "\n") + body[sectionEnd:]
|
||||
}
|
||||
return body[:sectionStart] + strings.TrimRight(newContent, "\n") + "\n"
|
||||
}
|
||||
|
||||
rest := body[sectionStart+headerLen:]
|
||||
nextSection := strings.Index(rest, "\n[")
|
||||
var sectionEnd int
|
||||
if nextSection >= 0 {
|
||||
sectionEnd = sectionStart + headerLen + nextSection
|
||||
} else {
|
||||
sectionEnd = len(body)
|
||||
}
|
||||
|
||||
return body[:sectionStart] + newContent + body[sectionEnd:]
|
||||
}
|
||||
|
||||
func renderScopeForPath(path string) RenderScope {
|
||||
if isUserConfigPath(path) {
|
||||
return RenderScopeUser
|
||||
|
||||
@@ -522,6 +522,350 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// RenderTOMLProjectDelta generates TOML containing only the sections and fields
|
||||
// that differ from built-in defaults. Unlike RenderTOMLForScope (which renders
|
||||
// the full config with comments), this emits clean TOML that can be surgically
|
||||
// merged into an existing project config file via replaceTOMLSection.
|
||||
func RenderTOMLProjectDelta(c *Config) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
d := Default()
|
||||
var b strings.Builder
|
||||
|
||||
// Top-level scalar fields
|
||||
if c.DefaultModel != d.DefaultModel {
|
||||
fmt.Fprintf(&b, "default_model = %q\n", c.DefaultModel)
|
||||
}
|
||||
if c.Language != "" && c.Language != d.Language {
|
||||
fmt.Fprintf(&b, "language = %q\n", c.Language)
|
||||
}
|
||||
|
||||
// [ui] section — whole-section comparison
|
||||
if !reflect.DeepEqual(c.UI, d.UI) {
|
||||
b.WriteString("[ui]\n")
|
||||
if c.UI.Theme != d.UI.Theme {
|
||||
fmt.Fprintf(&b, "theme = %q\n", c.UITheme())
|
||||
}
|
||||
if s := c.UIThemeStyle(); s != "" && s != d.UIThemeStyle() {
|
||||
fmt.Fprintf(&b, "theme_style = %q\n", s)
|
||||
}
|
||||
if l := c.UIShortcutLayout(); l != "classic" {
|
||||
fmt.Fprintf(&b, "shortcut_layout = %q\n", l)
|
||||
}
|
||||
if c.UI.CloseBehavior != d.UI.CloseBehavior {
|
||||
fmt.Fprintf(&b, "close_behavior = %q\n", c.DesktopCloseBehavior())
|
||||
}
|
||||
if c.UI.ShowReasoning != d.UI.ShowReasoning {
|
||||
fmt.Fprintf(&b, "show_reasoning = %v\n", c.UI.ShowReasoning)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [network] section
|
||||
if !reflect.DeepEqual(c.Network, d.Network) {
|
||||
b.WriteString("[network]\n")
|
||||
if c.Network.ProxyMode != d.Network.ProxyMode {
|
||||
fmt.Fprintf(&b, "proxy_mode = %q\n", c.NetworkProxyMode())
|
||||
}
|
||||
if c.Network.ProxyURL != "" {
|
||||
fmt.Fprintf(&b, "proxy_url = %q\n", c.Network.ProxyURL)
|
||||
}
|
||||
if c.Network.NoProxy != "" {
|
||||
fmt.Fprintf(&b, "no_proxy = %q\n", c.Network.NoProxy)
|
||||
}
|
||||
if c.Network.Proxy.Type != "" || c.Network.Proxy.Server != "" || c.Network.Proxy.Port > 0 || c.Network.Proxy.Username != "" || c.Network.Proxy.Password != "" {
|
||||
b.WriteString("[network.proxy]\n")
|
||||
pt := c.Network.Proxy.Type
|
||||
if pt == "" {
|
||||
pt = "socks5"
|
||||
}
|
||||
fmt.Fprintf(&b, "type = %q\n", pt)
|
||||
if c.Network.Proxy.Server != "" {
|
||||
fmt.Fprintf(&b, "server = %q\n", c.Network.Proxy.Server)
|
||||
}
|
||||
if c.Network.Proxy.Port > 0 {
|
||||
fmt.Fprintf(&b, "port = %d\n", c.Network.Proxy.Port)
|
||||
}
|
||||
if c.Network.Proxy.Username != "" {
|
||||
fmt.Fprintf(&b, "username = %q\n", c.Network.Proxy.Username)
|
||||
}
|
||||
if c.Network.Proxy.Password != "" {
|
||||
fmt.Fprintf(&b, "password = %q\n", c.Network.Proxy.Password)
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [agent] section — per-field comparison
|
||||
var agentBuf strings.Builder
|
||||
anyAgent := false
|
||||
|
||||
if sp := strings.TrimSpace(c.Agent.SystemPrompt); sp != "" && sp != d.Agent.SystemPrompt {
|
||||
agentBuf.WriteString("system_prompt = \"\"\"\n")
|
||||
agentBuf.WriteString(sp)
|
||||
agentBuf.WriteString("\"\"\"\n")
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.SystemPromptFile != "" && c.Agent.SystemPromptFile != d.Agent.SystemPromptFile {
|
||||
fmt.Fprintf(&agentBuf, "system_prompt_file = %q\n", c.Agent.SystemPromptFile)
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.Temperature != d.Agent.Temperature {
|
||||
fmt.Fprintf(&agentBuf, "temperature = %s\n", formatFloat(c.Agent.Temperature))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.ReasoningLanguage != d.Agent.ReasoningLanguage {
|
||||
if l := c.ReasoningLanguage(); l != "auto" {
|
||||
fmt.Fprintf(&agentBuf, "reasoning_language = %q\n", l)
|
||||
anyAgent = true
|
||||
}
|
||||
}
|
||||
if c.Agent.AutoPlanClassifier != "" && c.Agent.AutoPlanClassifier != d.Agent.AutoPlanClassifier {
|
||||
fmt.Fprintf(&agentBuf, "auto_plan_classifier = %q\n", c.Agent.AutoPlanClassifier)
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.SoftCompactRatio != d.Agent.SoftCompactRatio {
|
||||
fmt.Fprintf(&agentBuf, "soft_compact_ratio = %s\n", formatFloat(c.Agent.SoftCompactRatio))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.CompactRatio != d.Agent.CompactRatio {
|
||||
fmt.Fprintf(&agentBuf, "compact_ratio = %s\n", formatFloat(c.Agent.CompactRatio))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.CompactForceRatio != d.Agent.CompactForceRatio {
|
||||
fmt.Fprintf(&agentBuf, "compact_force_ratio = %s\n", formatFloat(c.Agent.CompactForceRatio))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.Keep != nil && !reflect.DeepEqual(c.Agent.Keep, d.Agent.Keep) {
|
||||
fmt.Fprintf(&agentBuf, "keep = %s\n", renderStringArray(c.Agent.Keep))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.RecentKeep > 0 && c.Agent.RecentKeep != d.Agent.RecentKeep {
|
||||
fmt.Fprintf(&agentBuf, "recent_keep = %d\n", c.Agent.RecentKeep)
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.ColdResumePrune != d.Agent.ColdResumePrune {
|
||||
fmt.Fprintf(&agentBuf, "cold_resume_prune = %v\n", c.ColdResumePruneEnabled())
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.PlannerModel != "" && c.Agent.PlannerModel != d.Agent.PlannerModel {
|
||||
fmt.Fprintf(&agentBuf, "planner_model = %q\n", c.Agent.PlannerModel)
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.SubagentModel != "" && c.Agent.SubagentModel != d.Agent.SubagentModel {
|
||||
fmt.Fprintf(&agentBuf, "subagent_model = %q\n", c.Agent.SubagentModel)
|
||||
anyAgent = true
|
||||
}
|
||||
if len(c.Agent.SubagentModels) > 0 && !reflect.DeepEqual(c.Agent.SubagentModels, d.Agent.SubagentModels) {
|
||||
fmt.Fprintf(&agentBuf, "subagent_models = %s\n", renderStringMap(c.Agent.SubagentModels))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.SubagentEffort != "" && c.Agent.SubagentEffort != d.Agent.SubagentEffort {
|
||||
fmt.Fprintf(&agentBuf, "subagent_effort = %q\n", c.Agent.SubagentEffort)
|
||||
anyAgent = true
|
||||
}
|
||||
if len(c.Agent.SubagentEfforts) > 0 && !reflect.DeepEqual(c.Agent.SubagentEfforts, d.Agent.SubagentEfforts) {
|
||||
fmt.Fprintf(&agentBuf, "subagent_efforts = %s\n", renderStringMap(c.Agent.SubagentEfforts))
|
||||
anyAgent = true
|
||||
}
|
||||
if c.Agent.OutputStyle != "" && c.Agent.OutputStyle != d.Agent.OutputStyle {
|
||||
fmt.Fprintf(&agentBuf, "output_style = %q\n", c.Agent.OutputStyle)
|
||||
anyAgent = true
|
||||
}
|
||||
|
||||
if anyAgent {
|
||||
b.WriteString("[agent]\n")
|
||||
b.WriteString(agentBuf.String())
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [[providers]] — include user-defined providers that aren't built-in
|
||||
proj := projectScopedConfigForRender(c)
|
||||
if proj != nil && len(proj.Providers) > 0 && !reflect.DeepEqual(proj.Providers, d.Providers) {
|
||||
for _, p := range proj.Providers {
|
||||
b.WriteString("[[providers]]\n")
|
||||
fmt.Fprintf(&b, "name = %q\n", p.Name)
|
||||
fmt.Fprintf(&b, "kind = %q\n", p.Kind)
|
||||
fmt.Fprintf(&b, "base_url = %q\n", p.BaseURL)
|
||||
if len(p.Models) > 0 {
|
||||
fmt.Fprintf(&b, "models = %s\n", renderStringArray(p.Models))
|
||||
if p.Default != "" {
|
||||
fmt.Fprintf(&b, "default = %q\n", p.Default)
|
||||
}
|
||||
} else if p.Model != "" {
|
||||
fmt.Fprintf(&b, "model = %q\n", p.Model)
|
||||
}
|
||||
if p.ModelsURL != "" {
|
||||
fmt.Fprintf(&b, "models_url = %q\n", p.ModelsURL)
|
||||
}
|
||||
fmt.Fprintf(&b, "api_key_env = %q\n", p.APIKeyEnv)
|
||||
if p.BalanceURL != "" {
|
||||
fmt.Fprintf(&b, "balance_url = %q\n", p.BalanceURL)
|
||||
}
|
||||
if p.ContextWindow > 0 {
|
||||
fmt.Fprintf(&b, "context_window = %d\n", p.ContextWindow)
|
||||
}
|
||||
if p.Price != nil {
|
||||
fmt.Fprintf(&b, "price = %s\n", renderPricingInline(p.Price))
|
||||
}
|
||||
if len(p.Prices) > 0 {
|
||||
fmt.Fprintf(&b, "prices = %s\n", renderPricingMap(p.Prices))
|
||||
}
|
||||
if p.Thinking != "" {
|
||||
fmt.Fprintf(&b, "thinking = %q\n", p.Thinking)
|
||||
}
|
||||
if p.Effort != "" {
|
||||
fmt.Fprintf(&b, "effort = %q\n", p.Effort)
|
||||
}
|
||||
if p.Vision {
|
||||
b.WriteString("vision = true\n")
|
||||
}
|
||||
if p.VisionModels != nil {
|
||||
fmt.Fprintf(&b, "vision_models = %s\n", renderStringArray(p.VisionModels))
|
||||
}
|
||||
if p.VisionDetail != "" {
|
||||
fmt.Fprintf(&b, "vision_detail = %q\n", p.VisionDetail)
|
||||
}
|
||||
if p.ReasoningProtocol != "" {
|
||||
fmt.Fprintf(&b, "reasoning_protocol = %q\n", p.ReasoningProtocol)
|
||||
}
|
||||
if len(p.SupportedEfforts) > 0 {
|
||||
fmt.Fprintf(&b, "supported_efforts = %s\n", renderStringArray(p.SupportedEfforts))
|
||||
}
|
||||
if p.DefaultEffort != "" {
|
||||
fmt.Fprintf(&b, "default_effort = %q\n", p.DefaultEffort)
|
||||
}
|
||||
if p.NoProxy {
|
||||
b.WriteString("no_proxy = true\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// [tools]
|
||||
if !reflect.DeepEqual(c.Tools, d.Tools) {
|
||||
b.WriteString("[tools]\n")
|
||||
if len(c.Tools.Enabled) > 0 {
|
||||
fmt.Fprintf(&b, "enabled = %s\n", renderStringArray(c.Tools.Enabled))
|
||||
}
|
||||
if c.Tools.BashTimeoutSeconds != nil && *c.Tools.BashTimeoutSeconds != 0 {
|
||||
fmt.Fprintf(&b, "bash_timeout_seconds = %d\n", *c.Tools.BashTimeoutSeconds)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [tools.background_jobs]
|
||||
if c.Tools.BackgroundJobs != d.Tools.BackgroundJobs {
|
||||
if c.Tools.BackgroundJobs.StalledWarningSeconds != nil && *c.Tools.BackgroundJobs.StalledWarningSeconds > 0 {
|
||||
b.WriteString("[tools.background_jobs]\n")
|
||||
fmt.Fprintf(&b, "stalled_warning_seconds = %d\n", *c.Tools.BackgroundJobs.StalledWarningSeconds)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// [lsp]
|
||||
if !reflect.DeepEqual(c.LSP, d.LSP) {
|
||||
renderLSPConfig(&b, c.LSP)
|
||||
}
|
||||
|
||||
// [skills]
|
||||
if !reflect.DeepEqual(c.Skills, d.Skills) {
|
||||
b.WriteString("[skills]\n")
|
||||
if len(c.Skills.Paths) > 0 {
|
||||
fmt.Fprintf(&b, "paths = %s\n", renderStringArray(c.Skills.Paths))
|
||||
}
|
||||
if len(c.Skills.ExcludedPaths) > 0 {
|
||||
fmt.Fprintf(&b, "excluded_paths = %s\n", renderStringArray(c.Skills.ExcludedPaths))
|
||||
}
|
||||
if c.Skills.MaxDepth != 0 {
|
||||
fmt.Fprintf(&b, "max_depth = %d\n", c.SkillMaxDepth())
|
||||
}
|
||||
if disabled := c.DisabledSkillNames(); len(disabled) > 0 {
|
||||
fmt.Fprintf(&b, "disabled_skills = %s\n\n", renderStringArray(disabled))
|
||||
}
|
||||
}
|
||||
|
||||
// [permissions]
|
||||
if !reflect.DeepEqual(c.Permissions, d.Permissions) {
|
||||
b.WriteString("[permissions]\n")
|
||||
mode := c.Permissions.Mode
|
||||
if mode == "" {
|
||||
mode = "ask"
|
||||
}
|
||||
if mode != "ask" {
|
||||
fmt.Fprintf(&b, "mode = %q\n", mode)
|
||||
}
|
||||
if len(c.Permissions.Deny) > 0 {
|
||||
fmt.Fprintf(&b, "deny = %s\n", renderStringArray(c.Permissions.Deny))
|
||||
}
|
||||
if len(c.Permissions.Allow) > 0 {
|
||||
fmt.Fprintf(&b, "allow = %s\n", renderStringArray(c.Permissions.Allow))
|
||||
}
|
||||
if len(c.Permissions.Ask) > 0 {
|
||||
fmt.Fprintf(&b, "ask = %s\n", renderStringArray(c.Permissions.Ask))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [sandbox]
|
||||
if !reflect.DeepEqual(c.Sandbox, d.Sandbox) {
|
||||
b.WriteString("[sandbox]\n")
|
||||
if c.Sandbox.WorkspaceRoot != "" {
|
||||
fmt.Fprintf(&b, "workspace_root = %q\n", c.Sandbox.WorkspaceRoot)
|
||||
}
|
||||
if len(c.Sandbox.AllowWrite) > 0 {
|
||||
fmt.Fprintf(&b, "allow_write = %s\n", renderStringArray(c.Sandbox.AllowWrite))
|
||||
}
|
||||
if c.BashMode() != "enforce" {
|
||||
fmt.Fprintf(&b, "bash = %q\n", c.BashMode())
|
||||
}
|
||||
if c.Sandbox.Network != d.Sandbox.Network {
|
||||
fmt.Fprintf(&b, "network = %v\n", c.Sandbox.Network)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [statusline]
|
||||
if !reflect.DeepEqual(c.Statusline, d.Statusline) {
|
||||
b.WriteString("[statusline]\n")
|
||||
if c.Statusline.Command != "" {
|
||||
fmt.Fprintf(&b, "command = %q\n", c.Statusline.Command)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// [[plugins]] — always include when set; replaces all existing entries
|
||||
for _, pl := range c.Plugins {
|
||||
b.WriteString("[[plugins]]\n")
|
||||
fmt.Fprintf(&b, "name = %q\n", pl.Name)
|
||||
if pl.Type != "" {
|
||||
fmt.Fprintf(&b, "type = %q\n", pl.Type)
|
||||
}
|
||||
if pl.Command != "" {
|
||||
fmt.Fprintf(&b, "command = %q\n", pl.Command)
|
||||
}
|
||||
if len(pl.Args) > 0 {
|
||||
fmt.Fprintf(&b, "args = %s\n", renderStringArray(pl.Args))
|
||||
}
|
||||
if pl.URL != "" {
|
||||
fmt.Fprintf(&b, "url = %q\n", pl.URL)
|
||||
}
|
||||
if len(pl.Headers) > 0 {
|
||||
fmt.Fprintf(&b, "headers = %s\n", renderStringMap(pl.Headers))
|
||||
}
|
||||
if len(pl.Env) > 0 {
|
||||
fmt.Fprintf(&b, "env = %s\n", renderStringMap(pl.Env))
|
||||
}
|
||||
if pl.AutoStart != nil {
|
||||
fmt.Fprintf(&b, "auto_start = %v\n", *pl.AutoStart)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderPricingInline(p *provider.Pricing) string {
|
||||
if p == nil {
|
||||
return "{}"
|
||||
|
||||
Reference in new issue
Block a user