Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5e03e03
feat(agent): light/balanced/delivery role settings with unified tools…
SivanCola Aug 11, 2026
33b9fc2
merge: refresh feature/agent-role-settings from origin/main-v2
SivanCola Aug 11, 2026
7d57a41
Merge main-v2 into agent role settings
SivanCola Aug 11, 2026
daec1e0
fix(agent): enforce role policy end to end
SivanCola Aug 11, 2026
b81ca7c
Merge latest main-v2 into agent role settings
SivanCola Aug 11, 2026
49f6fb7
chore(ci): reconcile budgets with latest main
SivanCola Aug 11, 2026
e5306e8
fix(ci): clear role-setting lint debt
SivanCola Aug 11, 2026
0edff6d
Merge latest main-v2 into agent role settings
SivanCola Aug 11, 2026
9a17c25
Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-…
SivanCola Aug 11, 2026
39b9aa5
Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-…
SivanCola Aug 11, 2026
97738e0
Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-…
SivanCola Aug 11, 2026
f7d8835
test(boot): assert spend gate by pause class
SivanCola Aug 11, 2026
ec2da98
Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-…
SivanCola Aug 11, 2026
83b3b04
Merge latest main-v2 into agent role settings
SivanCola Aug 11, 2026
6aa100e
fix(agent): enforce no-external across command variants
SivanCola Aug 11, 2026
bdb7fce
Merge context gauge fix into role settings
SivanCola Aug 11, 2026
22e152d
Merge external WebView2 smoke into role settings
SivanCola Aug 11, 2026
cebd0d5
Merge context overflow display into role settings
SivanCola Aug 11, 2026
46923b9
Rename role settings to execution settings
SivanCola Aug 11, 2026
7294d86
Update boot documentation contract
SivanCola Aug 11, 2026
0e1932d
Merge main-v2 into execution settings
SivanCola Aug 11, 2026
f4586c4
Merge latest main-v2 appearance fix
SivanCola Aug 11, 2026
3a8dbce
Merge latest main-v2 capability notices
SivanCola Aug 11, 2026
1713e5a
Merge latest main-v2 diagnostics and goal budgets
SivanCola Aug 11, 2026
d15bfcd
fix(desktop): clarify turn completion summaries
SivanCola Aug 11, 2026
9de6912
fix(desktop): omit completion receipts from transcript
SivanCola Aug 11, 2026
8e49c2a
fix(agent): surface completion gaps on demand
SivanCola Aug 11, 2026
c038bcf
Merge latest main-v2 into execution settings
SivanCola Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions desktop/agent_preset_persistence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"path/filepath"
"testing"

"reasonix/internal/agent"
"reasonix/internal/boot"
)

func TestSaveTabSessionMetaReplacesStaleAgentPreset(t *testing.T) {
path := filepath.Join(t.TempDir(), "session.jsonl")
meta, err := agent.EnsureBranchMeta(path)
if err != nil {
t.Fatal(err)
}
meta.AgentPreset = boot.AgentPresetBalanced
meta.TokenMode = boot.TokenModeFull
if err := agent.SaveBranchMetaPreserveUpdated(path, meta); err != nil {
t.Fatal(err)
}

if err := saveTabSessionMetaSnapshot(tabSessionMetaSnapshot{path: path, tokenMode: boot.TokenModeDelivery}); err != nil {
t.Fatal(err)
}
got, ok, err := agent.LoadBranchMeta(path)
if err != nil || !ok {
t.Fatalf("LoadBranchMeta = %+v, %v, %v", got, ok, err)
}
if got.AgentPreset != boot.AgentPresetDelivery || got.TokenMode != boot.TokenModeDelivery {
t.Fatalf("persisted role = preset:%q tokenMode:%q, want delivery dual-write", got.AgentPreset, got.TokenMode)
}
if restored := tabSessionProfileFromMeta(path, got).tokenMode; restored != boot.TokenModeDelivery {
t.Fatalf("restored tokenMode = %q, want delivery", restored)
}
}
187 changes: 62 additions & 125 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,12 @@ func (a *App) restoreOrBuildTabs() {
}
tab.model = entry.Model
tab.effort = cloneStringPtr(entry.Effort)
tab.tokenMode = boot.NormalizeTokenMode(entry.TokenMode)
// Prefer agentPreset; fall back to legacy tokenMode for one version.
if strings.TrimSpace(entry.AgentPreset) != "" {
tab.tokenMode = boot.TokenModeFromAgentPreset(entry.AgentPreset)
} else {
tab.tokenMode = boot.NormalizeTokenMode(entry.TokenMode)
}
tab.mode = persistedTabMode(entry.Mode)
// Validate the persisted goal against the session's goal-state
// sidecar: a typed /new or /clear rotates the session through the
Expand Down Expand Up @@ -2282,6 +2287,7 @@ func (a *App) clearActiveSessionRuntime(tab *WorkspaceTab, oldCtrl control.Sessi
WorkspaceRoot: snap.workspaceRoot,
SessionDir: sessionDirForSnapshot(snap),
EffortOverride: cloneStringPtr(snap.effort),
AgentPreset: boot.NormalizeAgentPreset(snap.currentTokenMode()),
TokenMode: snap.currentTokenMode(),
SharedHost: sharedHost,
CleanupPendingReconciler: reconcileDesktopCleanupPending,
Expand Down Expand Up @@ -4378,6 +4384,7 @@ func (a *App) buildSessionRebindCandidate(
WorkspaceRoot: root,
SessionDir: sessionDir,
EffortOverride: cloneStringPtr(source.effort),
AgentPreset: boot.NormalizeAgentPreset(runtimeProfile.tokenMode),
TokenMode: runtimeProfile.tokenMode,
SharedHost: sharedHost,
CleanupPendingReconciler: reconcileDesktopCleanupPending,
Expand Down Expand Up @@ -6739,9 +6746,12 @@ type Meta struct {
CollaborationMode string `json:"collaborationMode"`
ToolApprovalMode string `json:"toolApprovalMode"`
TokenMode string `json:"tokenMode"`
Goal string `json:"goal,omitempty"`
GoalStatus string `json:"goalStatus,omitempty"`
GoalRuntime *GoalRuntimeView `json:"goalRuntime,omitempty"`
// AgentPreset is the canonical role setting (light|balanced|delivery).
// TokenMode remains the dual-write legacy wire value for one version.
AgentPreset string `json:"agentPreset,omitempty"`
Goal string `json:"goal,omitempty"`
GoalStatus string `json:"goalStatus,omitempty"`
GoalRuntime *GoalRuntimeView `json:"goalRuntime,omitempty"`
// Nil means no authoritative snapshot; non-nil empty means clear the panel.
CanonicalTodos *[]evidence.TodoItem `json:"canonicalTodos,omitempty"`
}
Expand Down Expand Up @@ -6859,6 +6869,7 @@ func (a *App) MetaForTab(tabID string) Meta {
Bypass: autoApproveTools,
CollaborationMode: collaborationMode,
ToolApprovalMode: toolApprovalMode,
AgentPreset: boot.NormalizeAgentPreset(tokenMode),
TokenMode: tokenMode,
Goal: goal,
GoalStatus: goalStatus,
Expand Down Expand Up @@ -9993,6 +10004,7 @@ func (a *App) SetModelForTab(tabID, name string) (retErr error) {
WorkspaceRoot: snap.workspaceRoot,
SessionDir: sessionDirForSnapshot(snap),
EffortOverride: cloneStringPtr(effortOverride),
AgentPreset: boot.NormalizeAgentPreset(runtime.tokenMode),
TokenMode: runtime.tokenMode,
SharedHost: sharedHost,
CleanupPendingReconciler: reconcileDesktopCleanupPending,
Expand Down Expand Up @@ -10180,6 +10192,7 @@ func (a *App) SetEffortForTab(tabID, level string) error {
WorkspaceRoot: snap.workspaceRoot,
SessionDir: sessionDirForSnapshot(snap),
EffortOverride: &effort,
AgentPreset: boot.NormalizeAgentPreset(runtime.tokenMode),
TokenMode: runtime.tokenMode,
SharedHost: sharedHost,
CleanupPendingReconciler: reconcileDesktopCleanupPending,
Expand Down Expand Up @@ -10233,11 +10246,25 @@ func (a *App) SetEffortForTab(tabID, level string) error {
}

func (a *App) SetTokenMode(mode string) error {
return a.SetTokenModeForTab("", mode)
// Deprecated: dual-write alias for SetAgentPreset.
return a.SetAgentPreset(boot.NormalizeAgentPreset(mode))
}

func (a *App) SetTokenModeForTab(tabID, mode string) error {
mode = boot.NormalizeTokenMode(mode)
// Deprecated: dual-write alias for SetAgentPresetForTab.
return a.SetAgentPresetForTab(tabID, boot.NormalizeAgentPreset(mode))
}

// SetAgentPreset switches the active tab's execution setting (执行设定).
func (a *App) SetAgentPreset(preset string) error {
return a.SetAgentPresetForTab("", preset)
}

// SetAgentPresetForTab switches a tab's role setting without rebuilding the
// controller. Active turns, background jobs, and pending approvals/asks refuse.
func (a *App) SetAgentPresetForTab(tabID, preset string) error {
preset = boot.NormalizeAgentPreset(preset)
legacyMode := boot.TokenModeFromAgentPreset(preset)
tab := a.tabByID(tabID)
if tab == nil {
if strings.TrimSpace(tabID) == "" {
Expand All @@ -10246,12 +10273,12 @@ func (a *App) SetTokenModeForTab(tabID, mode string) error {
return fmt.Errorf("tab %q not found", tabID)
}
a.mu.RLock()
currentMode := boot.NormalizeTokenMode(tab.tokenMode)
currentPreset := boot.NormalizeAgentPreset(tab.tokenMode)
a.mu.RUnlock()
if mode == currentMode {
if preset == currentPreset {
return nil
}
// Build+swap path; serialize with the other rebuild paths (see runtimeRebuildMu).
// Serialize with model/effort/settings rebuilds and turn admission.
a.runtimeRebuildMu.Lock()
defer a.runtimeRebuildMu.Unlock()
tab.turnStartMu.Lock()
Expand All @@ -10260,116 +10287,46 @@ func (a *App) SetTokenModeForTab(tabID, mode string) error {
if prevPath == "" {
prevPath = a.currentSessionPathFor(tab)
}
// Recomputing prevPath after this attach would be a dead store: it is
// unconditionally derived again after ensureTabControllerWorkspace below.
if a.controllerForTab(tab) == nil && prevPath != "" {
a.attachExistingSessionRuntime(tab, prevPath, a.ctx)
}
if err := rebuildControllerActiveWorkErrorFor(a.controllerForTab(tab), "token mode"); err != nil {
if err := rebuildControllerActiveWorkErrorFor(a.controllerForTab(tab), "执行设定"); err != nil {
return err
}
if err := a.ensureTabControllerWorkspace(tab); err != nil {
return err
}
prevPath = a.reconciledSessionPathForTab(tab)
if prevPath == "" {
prevPath = a.currentSessionPathFor(tab)
}
if a.controllerForTab(tab) == nil && prevPath != "" && a.attachExistingSessionRuntime(tab, prevPath, a.ctx) {
prevPath = a.reconciledSessionPathForTab(tab)
if prevPath == "" {
prevPath = a.currentSessionPathFor(tab)
}
if err := rebuildControllerActiveWorkErrorFor(a.controllerForTab(tab), "token mode"); err != nil {
return err
}
}
modelRef, fallback, err := a.resolvedModelForTab(tab)
if err != nil {
return err
}
snap := a.tabRuntimeSnapshot(tab)
runtime := snap.normalizedRuntime()
runtime.tokenMode = mode
if fallback && strings.TrimSpace(snap.model) != "" {
a.noticeForTab(tab.ID, fmt.Sprintf("model %q is no longer available; switched to %s", snap.model, modelRef))
ctrl := a.controllerForTab(tab)
if ctrl == nil {
// No live controller yet: persist the choice for the next build.
a.mu.Lock()
tab.tokenMode = legacyMode
a.mu.Unlock()
a.persistTabTokenMode(tab, legacyMode)
return nil
}
// In-place switch: update the agent atomic value, tab state, and dual-write
// persistence without boot.Build.
ctrl.SetAgentPreset(preset)
a.mu.Lock()
tab.tokenMode = legacyMode
a.mu.Unlock()
a.persistTabTokenMode(tab, legacyMode)
a.notifyTabRuntimeRebuilt(tab)
return nil
}

var carried []provider.Message
oldCtrl := a.controllerForTab(tab)
if oldCtrl != nil {
if prevPath == "" {
prevPath = oldCtrl.SessionPath()
}
if err := a.ensureTabSessionLeaseForRebuild(tab, prevPath, "token mode"); err != nil {
return err
}
if err := a.snapshotTabForAction(tab, "changing token mode"); err != nil {
return err
}
prevPath = sessionPathAfterSnapshot(oldCtrl, prevPath)
carried = oldCtrl.History()
}
sharedHost := a.lookupSharedHost(snap.sharedHostKey)
newCtrl, err := boot.Build(a.bootContext(), boot.Options{
Model: modelRef,
RequireKey: false,
StatsSource: "desktop",
TaskStore: a.taskStore(),
OnConfigLoadWarnings: a.configLoadWarningsHandler(),
Sink: snap.sink,
WorkspaceRoot: snap.workspaceRoot,
SessionDir: sessionDirForSnapshot(snap),
EffortOverride: cloneStringPtr(snap.effort),
TokenMode: mode,
SharedHost: sharedHost,
CleanupPendingReconciler: reconcileDesktopCleanupPending,
SubagentParentLive: a.subagentParentProbeForBuild(tab),
SessionRecoveryMeta: a.tabSessionRecoveryMeta(tab),
OnSessionRecovered: a.handleTabSessionRecovered(tab),
// Same logical session: keep the private temporary directory across
// token-mode switches (Issue #7575).
SessionTemp: sessionTempFromController(oldCtrl),
})
if err != nil {
return err
}
a.bindControllerDisplayRecorder(newCtrl)
configureControllerRuntime(newCtrl, oldCtrl, runtime)
path := agent.ContinueSessionPath(prevPath, newCtrl.SessionDir(), newCtrl.Label())
if err := a.ensureTabSessionLeaseForRebuild(tab, path, "token mode"); err != nil {
newCtrl.Close()
return err
}
restoredRuntime, err := resumeControllerRuntimeWithMessages(newCtrl, carried, path, runtime)
if err != nil {
newCtrl.Close()
return err
// persistTabTokenMode dual-writes tokenMode (legacy) for the role setting.
// Failures keep the in-memory preset (already applied).
func (a *App) persistTabTokenMode(tab *WorkspaceTab, mode string) {
if a == nil || tab == nil {
return
}
_ = mode
a.mu.Lock()
if current := a.tabs[tab.ID]; current != tab {
a.mu.Unlock()
newCtrl.Close()
tab.releaseSessionLease()
return fmt.Errorf("tab %q changed while switching token mode; retry", tab.ID)
}
tab.Ctrl = newCtrl
tab.model = modelRef
tab.Label = newCtrl.Label()
applyNormalizedRuntimeToTabLocked(tab, restoredRuntime)
clearTabStartupError(tab)
tab.Ready = true
a.supersedeTabBuildLocked(tab)
a.saveTabsLocked()
a.mu.Unlock()
if oldCtrl != nil {
oldCtrl.Close()
}
// The rebuilt runtime reflects the on-disk config; drop any deferred refresh.
a.clearDeferredRebuild(tab.ID)
a.persistTabSessionPath(tab, path)
a.notifyTabRuntimeRebuilt(tab)
return nil
_ = a.saveTabSessionMetaForCurrentSession(tab)
}

func (a *App) applyProviderEffortConfig(entry *config.ProviderEntry, effort string) error {
Expand Down Expand Up @@ -10948,26 +10905,6 @@ func (a *App) currentProviderEntryForTab(tabID string) (*config.ProviderEntry, e
return entry, nil
}

func (a *App) resolvedModelForTab(tab *WorkspaceTab) (string, bool, error) {
if tab == nil {
return "", false, fmt.Errorf("no active tab")
}
cfg, err := config.LoadForRoot(tab.WorkspaceRoot)
if err != nil {
return "", false, err
}
ref := strings.TrimSpace(tab.model)
if ref == "" {
ref = cfg.DefaultModel
}
config.NormalizeLegacyMimoCustomProvidersForRefs(cfg, ref)
resolved, fallback, ok := cfg.ResolveModelWithFallback(ref)
if !ok {
return "", false, fmt.Errorf("unknown model %q", ref)
}
return resolved, fallback, nil
}

func (a *App) withActiveWorkspace(fn func() (string, error)) (string, error) {
var result string
err := a.withActiveWorkspaceDo(func() error {
Expand Down
Loading
Loading