返回 DeepSeek-Reasonix
session_preset_store.go
根目录 / desktop / session_preset_store.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "log/slog"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11 )
12
13 const sessionPresetsFileName = "desktop-session-presets.json"
14
15 type sessionPresetsFile struct {
16 Version int `json:"version"`
17 Sessions map[string]string `json:"sessions"`
18 }
19
20 // sessionPresetStore holds the permission preset the user explicitly chose for
21 // each canonical session, keyed by SessionID. Only record writes to it, and only
22 // the explicit preset-change entry points call record; a session is restored
23 // to its own record or, without one, to the new-session default.
24 type sessionPresetStore struct {
25 mu sync.Mutex
26 loaded bool
27 unsaved bool
28 recorded map[string]string
29 }
30
31 func newSessionPresetStore() *sessionPresetStore {
32 return &sessionPresetStore{}
33 }
34
35 func sessionPresetsPath() string {
36 return filepath.Join(desktopConfigDir(), sessionPresetsFileName)
37 }
38
39 func (s *sessionPresetStore) loadLocked() {
40 if s.loaded {
41 return
42 }
43 s.loaded = true
44 s.recorded = map[string]string{}
45 body, err := os.ReadFile(sessionPresetsPath())
46 if errors.Is(err, os.ErrNotExist) {
47 return
48 }
49 var file sessionPresetsFile
50 if err == nil {
51 err = json.Unmarshal(body, &file)
52 }
53 if err != nil {
54 // Unreadable records restore nothing: every session opens at the default.
55 slog.Warn("desktop: session permission presets unreadable", "err", err)
56 return
57 }
58 for id, preset := range file.Sessions {
59 if id = strings.TrimSpace(id); id != "" {
60 s.recorded[id] = normalizeToolApprovalMode(preset)
61 }
62 }
63 }
64
65 // restore returns the preset recorded for sessionID, or fallback when the
66 // session has none.
67 func (s *sessionPresetStore) restore(sessionID, fallback string) string {
68 sessionID = strings.TrimSpace(sessionID)
69 fallback = normalizeToolApprovalMode(fallback)
70 if s == nil || sessionID == "" {
71 return fallback
72 }
73 s.mu.Lock()
74 defer s.mu.Unlock()
75 s.loadLocked()
76 if preset, ok := s.recorded[sessionID]; ok {
77 return preset
78 }
79 return fallback
80 }
81
82 // record stores an explicit user choice for the session it was made in.
83 func (s *sessionPresetStore) record(sessionID, preset string) {
84 sessionID = strings.TrimSpace(sessionID)
85 if s == nil || sessionID == "" {
86 return
87 }
88 preset = normalizeToolApprovalMode(preset)
89 s.mu.Lock()
90 defer s.mu.Unlock()
91 s.loadLocked()
92 if recorded, ok := s.recorded[sessionID]; ok && recorded == preset && !s.unsaved {
93 return
94 }
95 s.recorded[sessionID] = preset
96 s.writeLocked()
97 }
98
99 func (s *sessionPresetStore) forget(sessionID string) {
100 sessionID = strings.TrimSpace(sessionID)
101 if s == nil || sessionID == "" {
102 return
103 }
104 s.mu.Lock()
105 defer s.mu.Unlock()
106 s.loadLocked()
107 if _, ok := s.recorded[sessionID]; !ok && !s.unsaved {
108 return
109 }
110 delete(s.recorded, sessionID)
111 s.writeLocked()
112 }
113
114 func (s *sessionPresetStore) writeLocked() {
115 body, err := json.MarshalIndent(sessionPresetsFile{Version: 1, Sessions: s.recorded}, "", " ")
116 if err == nil {
117 err = os.MkdirAll(desktopConfigDir(), 0o700)
118 }
119 if err == nil {
120 err = writeFileAtomic(sessionPresetsPath(), body, 0o600)
121 }
122 // The file must not go on holding a wider preset than this process runs
123 // with: a failed write drops or empties it, so a restart opens at the
124 // default, and the next record or forget writes the full set again.
125 s.unsaved = err != nil
126 if err != nil {
127 slog.Warn("desktop: session permission presets not saved", "err", err)
128 discardSessionPresetsFile()
129 }
130 }
131
132 // discardSessionPresetsFile removes the file, or empties it in place when its
133 // directory refuses the removal; an empty file restores nothing.
134 func discardSessionPresetsFile() {
135 path := sessionPresetsPath()
136 rmErr := os.Remove(path)
137 if rmErr == nil || errors.Is(rmErr, os.ErrNotExist) {
138 return
139 }
140 if err := os.Truncate(path, 0); err != nil {
141 slog.Warn("desktop: stale session permission presets not discarded", "remove", rmErr, "truncate", err)
142 }
143 }
144
144 lines GO