Skip to content

Commit 7d728e2

Browse files
digiogithubclaude
andcommitted
fix(webui): persist persona, coder auto-compact, LSP autostart and UI language settings
- Persona: store the active persona in [Persona] Active (project over global, default assistant, "auto" sentinel) and restore it on startup; WebUI, TUI and ACP changes persist through SetAndPersistActivePersona. - Auto-compact: Agent.AutoCompact is now *bool and overrides the global AutoCompact when set; model switches no longer rebuild the agent and drop AutoCompact, threshold, thinking mode or context window override. - LSP: UpdateLSP keeps Autostart, Languages and Filenames; PUT keeps stored Options the UI never sends. - UI language: saved as a server-side UI preference with a localStorage cache, applied immediately and no longer reset by fetchSettings. - Drop the stale per-agent AutoCompact = false overrides from .pando.toml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JsuQ7suHKMiaZUTB9K6qQ
1 parent 6b3a8bb commit 7d728e2

26 files changed

Lines changed: 704 additions & 64 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
created_at: 2026-09-30T11:59:14.006242063Z
3+
updated_at: 2026-09-30T11:59:14.006242063Z
4+
tags:
5+
- analysis
6+
- webui
7+
- config
8+
- persistence
9+
- pando
10+
---
11+
# Analysis: WebUI settings that do not persist (2026-09-30)
12+
13+
Related: [[evaluator-config-json-keys]], [[feature_webui_simple_chat_shell]], [[lsp_ondemand_runtime_and_activation_triggers]]
14+
15+
Four reported bugs, analysed by parallel subagents; key lines spot-checked. NOT FIXED YET.
16+
17+
## 1. Persona selector lost after restarting pando desktop (backend)
18+
- `PersonaSelector.tsx` -> PUT `/api/v1/personas/active` -> `handleSetActivePersona` (`internal/api/handlers_personas.go`) -> `agent.SetActivePersona` only sets the in-memory global `activePersonaName` (`internal/llm/agent/persona_selector.go:33`). Nothing is written to disk.
19+
- On startup `internal/app/app.go:857-860` resets it to "assistant" (and "Auto"/empty also becomes "assistant").
20+
- Fix: persist in `webui-prefs.json` (UIPrefs `activePersona *string`, the chatMode pattern in `handlers_ui_prefs.go`) or in config with json+toml tags; restore it in app.go before the "assistant" default; TUI/ACP setters too. Decide global vs per-project.
21+
22+
## 2. Coder autoCompact reverts (backend)
23+
- WebUI PUT `/api/v1/config/agents` + `UpdateAgent` work (round-trip verified).
24+
- `setAgentModel` (`internal/config/config.go:~4650`) rebuilds `Agent{Model, MaxTokens, ReasoningEffort}`, dropping AutoCompact, AutoCompactThreshold, ThinkingMode, ContextWindowOverride, and writes it to the TOML. `agentInheritingModel` (`config.go:3961`) does the same for propagated agents. Any model change (chat picker, /model, setup, TUI) wipes the toggle.
25+
- Fix: copy the existing agent and set Model only; add a regression test.
26+
- Side issue: the runtime (`agent.go:~2335`) ORs global `AutoCompact` with the per-agent flag, so turning coder off does nothing while the global is true.
27+
28+
## 3. LSP Autostart not saved (backend)
29+
- `LSPSettings.tsx` modal -> PUT `/api/v1/config/lsp` -> `handlePutConfigLSP` copies the fields correctly, but `UpdateLSP` (`config.go:5538`) builds `LSPConfig{Disabled, Command, Args, Options}`, dropping Autostart, Languages and Filenames. The TUI has the same bug.
30+
- Fix: copy those fields; preserve the old Options when the request sends none; add a test.
31+
32+
## 4. Language reverts even when switching settings sections (frontend + backend gap)
33+
- UI-only i18n language. `settingsStore.ts` comment: "UI-only, not persisted". The backend `SettingsResponse`/`SettingsUpdateRequest` have no language field.
34+
- `GeneralSettings.tsx:41-43` runs `fetchSettings()` on mount, which merges `{...DEFAULTS, ...data}` and resets it to 'en'; `useLanguageSync` then applies 'en'. `i18n/index.ts:34` hardcodes `lng: 'en'`.
35+
- Fix: a dedicated `setLanguage` that applies immediately, with a server-side UI pref (`/api/v1/ui/preferences`, like chatMode) and a localStorage cache; `fetchSettings` must not overwrite it; i18n init reads the saved value.
36+
37+
## Common pattern
38+
Three are Go functions that rebuild structs field by field and silently drop newer fields (UpdateLSP, setAgentModel, agentInheritingModel). Prefer copying the existing struct and mutating it. UI state that must persist in desktop must live server-side (the random port breaks localStorage).
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
created_at: 2026-09-30T12:08:05.11681582Z
3+
updated_at: 2026-09-30T12:08:05.11681582Z
4+
tags:
5+
- fix
6+
- webui
7+
- config
8+
- persistence
9+
- persona
10+
- lsp
11+
- i18n
12+
---
13+
# Fix: WebUI settings persistence (persona, coder autoCompact, LSP autostart, UI language) — 2026-09-30
14+
15+
Analysis: [[webui-settings-persistence-bugs-2026-09-30]]. Related: [[evaluator-config-json-keys]].
16+
17+
User rule for persona + autoCompact: the effective value is the project `.pando.toml` value if set, else the global value; changes persist per project (through `updateCfgFile`, which writes the project file when it exists).
18+
19+
## Persona
20+
- New `internal/config/persona_active.go`: `PersonaConfig{Active}` (`[Persona] Active`, json `persona.active`), sentinel `"auto"` = explicit Auto, empty = never chosen; `ActivePersonaChoice`, `EffectiveActivePersona` (fallback `assistant`), `UpdateActivePersona` (rolls back on write error).
21+
- `Config.Persona` field added in config.go.
22+
- `agent.SetAndPersistActivePersona` (persona_selector.go) used by the TUI (`tui.go`), ACP adapters (`cmd/root.go`, `internal/app/app.go`) and the API PUT handler (`handlers_personas.go`, 500 on persist failure).
23+
- Startup (`app.go`) restores the effective persona; if it is unknown, logs a warning and falls back to assistant.
24+
- `PersonaSelector.tsx` shows an error toast when the PUT fails.
25+
26+
## Coder autoCompact
27+
- `Agent.AutoCompact` is now `*bool` (nil = inherit global). `config.ResolveAutoCompact(global, agent)`: the per-agent value OVERRIDES the global one (was OR).
28+
- `setAgentModel` and `agentInheritingModel` copy the existing Agent and change only Model (they used to drop AutoCompact/Threshold/ThinkingMode/ContextWindowOverride on every model switch).
29+
- Runtime: `agent.go` shouldCompact and the `tui.go` 95% trigger use the resolver. TUI settings shows the resolved value.
30+
- API `handlers_config.go`: GET returns the effective value; PUT stores an explicit per-agent value only when it differs from the effective one.
31+
- Init templates (`internal/config/init.go`, `cmd/init.go`) no longer write `AutoCompact = false` for coder.
32+
- CAVEAT: existing TOMLs with `AutoCompact = false` per agent (written by the old bug, e.g. this repo's .pando.toml) are now explicit overrides that beat global true. Delete those lines to inherit.
33+
34+
## LSP
35+
- `UpdateLSP` now copies Autostart, Languages and Filenames (clones the slices).
36+
- `handlePutConfigLSP` preserves the stored Options (the UI never sends them).
37+
38+
## UI language
39+
- `UIPrefs.Language` (`json:"language"`) in `handlers_ui_prefs.go` -> `~/.config/pando/webui-prefs.json` (global per machine); empty is ignored, >16 chars returns 400.
40+
- settingsStore: `setLanguage` (applies immediately, not dirty, localStorage `pando_language` cache, fire-and-forget PUT `/api/v1/ui/preferences`), `hydrateLanguage` with a `languageChosen` guard (called in MainLayout after hydrateChatMode); `fetchSettings` preserves language; `saveSettings` no longer sends it.
41+
- `GeneralSettings.tsx` uses setLanguage; `i18n/index.ts` reads the cached value at init.
42+
43+
## Tests / verification
44+
New tests: `internal/config/agent_persistence_test.go`, `internal/config/persona_active_test.go`, `internal/api/handlers_config_lsp_persist_test.go`, `internal/api/handlers_personas_persist_test.go`, `TestUIPrefsLanguageRoundTrip`.
45+
`go build ./...`, `go vet`, `go test ./internal/config ./internal/api ./internal/llm/agent ./internal/app ./internal/tui/...` all ok; `web-ui npx tsc --noEmit` ok. Not run: manual desktop check, `npm run build`.

‎.pando.toml‎

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,14 @@ Tools = []
3333

3434
[Agents]
3535
[Agents.cli-assist]
36-
AutoCompact = false
3736
AutoCompactThreshold = 0.0
3837
ContextWindowOverride = 0
3938
MaxTokens = 0
40-
Model = 'openrouter.deepseek/deepseek-v4-flash-0731'
39+
Model = 'copilot.gpt-6-luna'
4140
ReasoningEffort = ''
4241
ThinkingMode = ''
4342

4443
[Agents.cliassist]
45-
AutoCompact = false
4644
AutoCompactThreshold = 0.0
4745
ContextWindowOverride = 0
4846
MaxTokens = 0
@@ -51,7 +49,6 @@ ReasoningEffort = ''
5149
ThinkingMode = ''
5250

5351
[Agents.coder]
54-
AutoCompact = false
5552
AutoCompactThreshold = 0.0
5653
ContextWindowOverride = 0
5754
MaxTokens = 0
@@ -60,47 +57,42 @@ ReasoningEffort = ''
6057
ThinkingMode = ''
6158

6259
[Agents.context-enricher]
63-
AutoCompact = false
6460
AutoCompactThreshold = 0.0
6561
ContextWindowOverride = 0
6662
MaxTokens = 0
67-
Model = 'ollama.nova4b:latest'
63+
Model = 'copilot.gpt-6-luna'
6864
ReasoningEffort = ''
6965
ThinkingMode = ''
7066

7167
[Agents.persona-selector]
72-
AutoCompact = false
7368
AutoCompactThreshold = 0.0
7469
ContextWindowOverride = 0
7570
MaxTokens = 0
76-
Model = 'openrouter.deepseek/deepseek-v4-flash-0731'
71+
Model = 'copilot.gpt-6-luna'
7772
ReasoningEffort = ''
7873
ThinkingMode = ''
7974

8075
[Agents.summarizer]
81-
AutoCompact = false
8276
AutoCompactThreshold = 0.0
8377
ContextWindowOverride = 0
8478
MaxTokens = 0
85-
Model = 'copilot.mai-code-1.1-flash'
79+
Model = 'copilot.gpt-6-luna'
8680
ReasoningEffort = ''
8781
ThinkingMode = ''
8882

8983
[Agents.task]
90-
AutoCompact = false
9184
AutoCompactThreshold = 0.0
9285
ContextWindowOverride = 0
9386
MaxTokens = 0
94-
Model = 'copilot.mai-code-1.1-flash'
87+
Model = 'copilot.gpt-6-luna'
9588
ReasoningEffort = ''
9689
ThinkingMode = ''
9790

9891
[Agents.title]
99-
AutoCompact = false
10092
AutoCompactThreshold = 0.0
10193
ContextWindowOverride = 0
10294
MaxTokens = 0
103-
Model = 'copilot.gpt-5-mini'
95+
Model = 'copilot.gpt-6-luna'
10496
ReasoningEffort = ''
10597
ThinkingMode = ''
10698

@@ -272,6 +264,7 @@ Enabled = false
272264
Args = []
273265
Command = ''
274266
Env = []
267+
NoSandbox = false
275268
Sandbox = false
276269
Timeout = ''
277270
Type = 'streamable-http'
@@ -297,6 +290,7 @@ Username = ''
297290
Args = []
298291
Command = ''
299292
Env = []
293+
NoSandbox = false
300294
Sandbox = false
301295
Timeout = '5m'
302296
Type = 'streamable-http'
@@ -306,6 +300,7 @@ URL = 'https://mcp.deepwiki.com/mcp'
306300
Args = ['mcp']
307301
Command = 'gintrack'
308302
Env = []
303+
NoSandbox = false
309304
Sandbox = false
310305
Timeout = ''
311306
Type = 'stdio'
@@ -331,6 +326,7 @@ Username = ''
331326
Args = ['-c', '.ai/mycommand-mcp.yaml']
332327
Command = 'mycommandmcp'
333328
Env = []
329+
NoSandbox = false
334330
Sandbox = false
335331
Timeout = ''
336332
Type = 'stdio'
@@ -464,7 +460,6 @@ KBConvertExtensions = []
464460
KBPath = './.kb'
465461
KBWatch = true
466462
KBWikiLinks = true
467-
MemoryAutoCapture = true
468463
MemoryContextEnrichmentEnabled = true
469464
MemoryContextMaxChars = 500
470465
MemoryContextMaxItems = 3
@@ -547,17 +542,33 @@ Timeout = 0
547542
[evaluator]
548543
alphaWeight = 0.8
549544
async = true
545+
backfillJudge = false
546+
backfillLimit = 50
550547
betaWeight = 0.2
551548
correctionsPatterns = ['(?i)\\bno[,.]?\\b', '(?i)\\bwrong\\b', '(?i)\\bincorrect\\b', "(?i)that'?s not", '(?i)not what i', '(?i)\\bmistake\\b', '(?i)\\bfix that\\b', '(?i)\\bvuelve a\\b', '(?i)\\bno era eso\\b', '(?i)\\bte equivocaste\\b']
552549
enabled = true
553550
explorationC = 1.41
551+
idleTimeout = '30m'
552+
includeSubagents = false
554553
judgePromptTemplate = ''
555554
maxSkills = 100
556555
maxTokensBaseline = 50
557556
minSessionsForUCB = 5
558557
model = 'openrouter.deepseek/deepseek-v4-flash-0731'
559558
provider = 'openrouter'
560559

560+
[evaluator.contextTrimmer]
561+
enabled = false
562+
minConfidence = 0.7
563+
564+
[evaluator.judge]
565+
dailyCalls = 20
566+
dailyTokens = 200000
567+
highReward = 0.8
568+
lowReward = 0.3
569+
maxTranscriptTokens = 6000
570+
minTurns = 4
571+
561572
[[evaluator.taskPatterns]]
562573
pattern = 'fix|bug|error|crash|exception|failing|panic|traceback'
563574
taskType = 'debug'
@@ -582,6 +593,18 @@ taskType = 'code'
582593
pattern = 'find|search|where\s+is|locate|grep\s+for'
583594
taskType = 'search'
584595

596+
[evaluator.templates]
597+
enabled = true
598+
599+
[evaluator.weights]
600+
cancels = 0.05
601+
endState = 0.1
602+
repetition = 0.05
603+
success = 0.8
604+
tokens = 0.2
605+
toolErrors = 0.1
606+
turns = 0.05
607+
585608
[[providerAccounts]]
586609
displayName = 'copilot'
587610
id = 'copilot'

‎cmd/init.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ Languages = [] # Leave empty to apply to all files; e.g. ['.go']
141141
Model = 'anthropic.claude-sonnet-4-6'
142142
MaxTokens = 0 # 0 = Auto (role default: 8192 tokens; override with a positive number)
143143
ReasoningEffort = '' # 'low' | 'medium' | 'high' (OpenAI o-series only)
144-
AutoCompact = false
145-
AutoCompactThreshold = 0.0 # 0.0 = use global AutoCompact setting
144+
# AutoCompact = true # optional per-agent override; when omitted the global AutoCompact applies
145+
AutoCompactThreshold = 0.0 # 0.0 = default threshold (0.85)
146146
147147
# Summariser — condenses long conversations to save tokens.
148148
[Agents.summarizer]

‎cmd/root.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ func (a *acpAgentAdapter) GetActivePersona() string {
906906

907907
// SetActivePersona sets the active persona by name (empty = clear).
908908
func (a *acpAgentAdapter) SetActivePersona(name string) error {
909-
return agent.SetActivePersona(name)
909+
return agent.SetAndPersistActivePersona(name)
910910
}
911911

912912
func (a *acpAgentAdapter) Summarize(ctx context.Context, sessionID string) (<-chan acpPkg.AgentEvent, error) {

‎internal/api/handlers_config.go‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func (s *Server) handleGetConfigAgents(w http.ResponseWriter, r *http.Request) {
170170
ResolvedMaxTokens: config.ResolveAgentMaxTokens(name, a, model),
171171
ReasoningEffort: a.ReasoningEffort,
172172
ThinkingMode: a.ThinkingMode,
173-
AutoCompact: a.AutoCompact,
173+
AutoCompact: config.ResolveAutoCompact(cfg.AutoCompact, a),
174174
AutoCompactThreshold: a.AutoCompactThreshold,
175175
ContextControls: config.AgentExposesContextControls(name),
176176
})
@@ -202,11 +202,19 @@ func (s *Server) handlePutConfigAgents(w http.ResponseWriter, r *http.Request) {
202202
MaxTokens: item.MaxTokens,
203203
ReasoningEffort: item.ReasoningEffort,
204204
ThinkingMode: item.ThinkingMode,
205-
AutoCompact: item.AutoCompact,
206205
AutoCompactThreshold: item.AutoCompactThreshold,
207206
}
208207
if cfg := config.Get(); cfg != nil {
209208
existing := cfg.Agents[name]
209+
// GET reports the effective value, so only persist a per-agent
210+
// override when the payload differs from what is already in effect;
211+
// otherwise the agent keeps inheriting (or keeps its own) setting.
212+
if config.ResolveAutoCompact(cfg.AutoCompact, existing) == item.AutoCompact {
213+
agent.AutoCompact = existing.AutoCompact
214+
} else {
215+
v := item.AutoCompact
216+
agent.AutoCompact = &v
217+
}
210218
// ContextWindowOverride is never carried by this API; keep the
211219
// configured value instead of resetting it on every save.
212220
agent.ContextWindowOverride = existing.ContextWindowOverride
@@ -218,6 +226,9 @@ func (s *Server) handlePutConfigAgents(w http.ResponseWriter, r *http.Request) {
218226
agent.AutoCompact = existing.AutoCompact
219227
agent.AutoCompactThreshold = existing.AutoCompactThreshold
220228
}
229+
} else {
230+
v := item.AutoCompact
231+
agent.AutoCompact = &v
221232
}
222233
if err := config.UpdateAgent(name, agent); err != nil {
223234
writeConfigError(w, http.StatusBadRequest, "failed to update agent "+item.Name+": "+err.Error(), err)
@@ -772,6 +783,12 @@ func (s *Server) handlePutConfigLSP(w http.ResponseWriter, r *http.Request) {
772783
Filenames: req.Filenames,
773784
Autostart: req.Autostart,
774785
}
786+
// The UI never sends Options; keep the stored ones instead of wiping them.
787+
if cfg := config.Get(); cfg != nil {
788+
if existing, ok := cfg.LSP[req.Language]; ok {
789+
lsp.Options = existing.Options
790+
}
791+
}
775792
if err := config.UpdateLSP(req.Language, lsp); err != nil {
776793
writeConfigError(w, http.StatusBadRequest, "failed to update LSP config: "+err.Error(), err)
777794
return
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/digiogithub/pando/internal/config"
11+
)
12+
13+
func TestPutConfigLSPKeepsAutostartAndOptions(t *testing.T) {
14+
resetProviderAccountsTestConfig(t)
15+
cfg := config.Get()
16+
cfg.LSP["go"] = config.LSPConfig{Command: "gopls", Options: map[string]any{"a": 1}}
17+
18+
s := &Server{}
19+
body := `{"language":"go","command":"gopls","args":[],"languages":[".go"],"autostart":true}`
20+
req := httptest.NewRequest(http.MethodPut, "/api/v1/config/lsp", bytes.NewBufferString(body))
21+
rec := httptest.NewRecorder()
22+
s.handlePutConfigLSP(rec, req)
23+
if rec.Code != http.StatusOK {
24+
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
25+
}
26+
27+
var resp struct {
28+
LSP []LSPConfigItem `json:"lsp"`
29+
}
30+
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
31+
t.Fatalf("decode: %v (%s)", err, rec.Body.String())
32+
}
33+
found := false
34+
for _, item := range resp.LSP {
35+
if item.Language == "go" {
36+
found = true
37+
if !item.Autostart || len(item.Languages) != 1 {
38+
t.Fatalf("item = %+v, want autostart and languages kept", item)
39+
}
40+
}
41+
}
42+
if !found {
43+
t.Fatalf("go LSP missing from response: %s", rec.Body.String())
44+
}
45+
if config.Get().LSP["go"].Options == nil {
46+
t.Fatal("existing Options were wiped by the PUT")
47+
}
48+
}

‎internal/api/handlers_personas.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"net/http"
66

7+
"github.com/digiogithub/pando/internal/config"
78
agentpkg "github.com/digiogithub/pando/internal/llm/agent"
89
)
910

@@ -58,6 +59,10 @@ func (s *Server) handleSetActivePersona(w http.ResponseWriter, r *http.Request)
5859
writeError(w, http.StatusBadRequest, err.Error())
5960
return
6061
}
62+
if err := config.UpdateActivePersona(req.Name); err != nil {
63+
writeError(w, http.StatusInternalServerError, "persona activated but not saved: "+err.Error())
64+
return
65+
}
6166

6267
writeJSON(w, http.StatusOK, map[string]string{
6368
"active": req.Name,

0 commit comments

Comments
 (0)