Skip to content

Commit 79cc85d

Browse files
committed
feat: reconcile with on-disk changes before writing configuration
1 parent d383150 commit 79cc85d

File tree

1 file changed

+91
-5
lines changed

1 file changed

+91
-5
lines changed

internal/config/configuration.go

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7+
"time"
8+
9+
"github.com/reugn/gemini-cli/gemini"
710
)
811

912
// Configuration contains the details of the application configuration.
@@ -14,9 +17,12 @@ type Configuration struct {
1417
// Data is the application data. This data is loaded from the configuration
1518
// file and is used to configure the application.
1619
Data *ApplicationData
20+
// lastModTime is the time the configuration file was last modified.
21+
lastModTime time.Time
1722
}
1823

1924
// NewConfiguration returns a new Configuration from a JSON file.
25+
// If the file does not exist, it is created with default values.
2026
func NewConfiguration(filePath string) (*Configuration, error) {
2127
configuration := &Configuration{
2228
filePath: filePath,
@@ -34,28 +40,108 @@ func NewConfiguration(filePath string) (*Configuration, error) {
3440
defer file.Close()
3541

3642
decoder := json.NewDecoder(file)
37-
err = decoder.Decode(configuration.Data)
38-
if err != nil {
43+
if err := decoder.Decode(configuration.Data); err != nil {
3944
return nil, fmt.Errorf("error decoding JSON: %w", err)
4045
}
4146

4247
return configuration, nil
4348
}
4449

4550
// Flush serializes and writes the configuration to the file.
51+
//
52+
// If the file is modified since the last load/flush, the configuration is re-read
53+
// and merged with the on-disk data.
4654
func (c *Configuration) Flush() error {
55+
// Reload the configuration if the file was modified since the last load/flush.
56+
if err := c.reloadIfStale(); err != nil {
57+
return err
58+
}
59+
60+
// Create the file if it does not exist.
4761
file, err := os.Create(c.filePath)
4862
if err != nil {
4963
return fmt.Errorf("error opening file: %w", err)
5064
}
5165
defer file.Close()
5266

67+
// Serialize the configuration data to the file.
5368
encoder := json.NewEncoder(file)
5469
encoder.SetIndent("", " ")
55-
err = encoder.Encode(c.Data)
56-
if err != nil {
70+
if err := encoder.Encode(c.Data); err != nil {
5771
return fmt.Errorf("error encoding JSON: %w", err)
5872
}
5973

60-
return file.Sync()
74+
// Sync the file to disk.
75+
if err := file.Sync(); err != nil {
76+
return fmt.Errorf("error syncing file: %w", err)
77+
}
78+
79+
// Get the file information.
80+
info, err := os.Stat(c.filePath)
81+
if err != nil {
82+
return fmt.Errorf("error stating file: %w", err)
83+
}
84+
85+
// Update the last modified time.
86+
c.lastModTime = info.ModTime()
87+
88+
return nil
89+
}
90+
91+
// reloadIfStale re-reads and merges the on-disk configuration if the file was
92+
// modified since the last load/flush.
93+
func (c *Configuration) reloadIfStale() error {
94+
info, err := os.Stat(c.filePath)
95+
if err != nil {
96+
if os.IsNotExist(err) { // ignore error if file does not exist
97+
return nil
98+
}
99+
return fmt.Errorf("error stating config file: %w", err)
100+
}
101+
102+
// If the file was not modified since the last load/flush, do nothing.
103+
if !c.lastModTime.IsZero() && !info.ModTime().After(c.lastModTime) {
104+
return nil
105+
}
106+
107+
// Re-read the configuration file.
108+
file, err := os.Open(c.filePath)
109+
if err != nil {
110+
return fmt.Errorf("error reopening config file: %w", err)
111+
}
112+
defer file.Close()
113+
114+
onDisk := newDefaultApplicationData()
115+
if err := json.NewDecoder(file).Decode(onDisk); err != nil {
116+
return fmt.Errorf("error decoding updated config: %w", err)
117+
}
118+
119+
// Merge the on-disk data into the current configuration.
120+
c.mergeApplicationData(onDisk)
121+
122+
return nil
123+
}
124+
125+
// mergeApplicationData merges on-disk data into the current configuration.
126+
func (c *Configuration) mergeApplicationData(onDisk *ApplicationData) {
127+
if onDisk == nil || c.Data == nil {
128+
return
129+
}
130+
131+
// The CLI never modifies these fields; always overwrite with the on-disk values.
132+
c.Data.SystemPrompts = onDisk.SystemPrompts
133+
c.Data.SafetySettings = onDisk.SafetySettings
134+
c.Data.Tools = onDisk.Tools
135+
136+
// Merge history records.
137+
if onDisk.History != nil {
138+
if c.Data.History == nil {
139+
c.Data.History = make(map[string][]*gemini.SerializableContent, len(onDisk.History))
140+
}
141+
for label, records := range onDisk.History {
142+
if _, exists := c.Data.History[label]; !exists {
143+
c.Data.History[label] = records
144+
}
145+
}
146+
}
61147
}

0 commit comments

Comments
 (0)