Skip to content

Commit c3a132a

Browse files
committed
test message
1 parent a3012f2 commit c3a132a

5 files changed

Lines changed: 140 additions & 0 deletions

File tree

cmd/iterate/features_tools.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,100 @@ func getDeniedList() []string {
6868
// agentPool is the shared agent pool for /swarm command.
6969
var agentPool *agent.Pool
7070

71+
// ---------------------------------------------------------------------------
72+
// Undo stack — snapshots of file contents before agent writes
73+
// ---------------------------------------------------------------------------
74+
75+
// undoSnapshot records the state of a single file before an agent modification.
76+
type undoSnapshot struct {
77+
Path string
78+
Content []byte // nil means the file did not exist (new file created)
79+
}
80+
81+
// undoFrame is one agent turn's worth of file modifications.
82+
type undoFrame []undoSnapshot
83+
84+
var (
85+
undoStack []undoFrame
86+
undoStackMu sync.Mutex
87+
// currentFrame accumulates snapshots for the in-progress turn.
88+
currentFrame undoFrame
89+
)
90+
91+
// beginUndoFrame starts a new undo frame for the current agent turn.
92+
func beginUndoFrame() {
93+
undoStackMu.Lock()
94+
defer undoStackMu.Unlock()
95+
currentFrame = undoFrame{}
96+
}
97+
98+
// commitUndoFrame pushes the current frame (if non-empty) onto the stack.
99+
func commitUndoFrame() {
100+
undoStackMu.Lock()
101+
defer undoStackMu.Unlock()
102+
if len(currentFrame) > 0 {
103+
undoStack = append(undoStack, currentFrame)
104+
currentFrame = nil
105+
}
106+
}
107+
108+
// captureFileSnapshot saves the current content of path into the active frame.
109+
// Called just before a write/edit tool overwrites the file.
110+
func captureFileSnapshot(path string) {
111+
undoStackMu.Lock()
112+
defer undoStackMu.Unlock()
113+
// Only capture once per path per frame.
114+
for _, s := range currentFrame {
115+
if s.Path == path {
116+
return
117+
}
118+
}
119+
content, err := os.ReadFile(path)
120+
if os.IsNotExist(err) {
121+
currentFrame = append(currentFrame, undoSnapshot{Path: path, Content: nil})
122+
} else if err == nil {
123+
currentFrame = append(currentFrame, undoSnapshot{Path: path, Content: content})
124+
}
125+
}
126+
127+
// performUndo restores the most recent undo frame.
128+
// Returns the list of restored paths, or an error.
129+
func performUndo() ([]string, error) {
130+
undoStackMu.Lock()
131+
defer undoStackMu.Unlock()
132+
if len(undoStack) == 0 {
133+
return nil, fmt.Errorf("nothing to undo")
134+
}
135+
frame := undoStack[len(undoStack)-1]
136+
undoStack = undoStack[:len(undoStack)-1]
137+
138+
var restored []string
139+
var errs []string
140+
for _, snap := range frame {
141+
if snap.Content == nil {
142+
// File was newly created — remove it.
143+
if err := os.Remove(snap.Path); err != nil && !os.IsNotExist(err) {
144+
errs = append(errs, fmt.Sprintf("remove %s: %v", snap.Path, err))
145+
continue
146+
}
147+
} else {
148+
if err := os.MkdirAll(filepath.Dir(snap.Path), 0o755); err != nil {
149+
errs = append(errs, fmt.Sprintf("mkdir %s: %v", snap.Path, err))
150+
continue
151+
}
152+
if err := os.WriteFile(snap.Path, snap.Content, 0o644); err != nil {
153+
errs = append(errs, fmt.Sprintf("restore %s: %v", snap.Path, err))
154+
continue
155+
}
156+
}
157+
restored = append(restored, snap.Path)
158+
}
159+
if len(errs) > 0 {
160+
return restored, fmt.Errorf("%s", strings.Join(errs, "; "))
161+
}
162+
return restored, nil
163+
}
164+
71165
// wrapToolsWithPermissions wraps tools that need approval in safe mode
72166
// and adds audit logging to all tools.
73167
func wrapToolsWithPermissions(tools []iteragent.Tool) []iteragent.Tool {
@@ -84,6 +178,13 @@ func wrapToolsWithPermissions(tools []iteragent.Tool) []iteragent.Tool {
84178

85179
trackSessionChanges(t.Name, args)
86180

181+
// Capture file snapshot before any write/edit so /undo can restore.
182+
if t.Name == "write_file" || t.Name == "edit_file" || t.Name == "create_file" {
183+
if p, ok := args["path"]; ok {
184+
captureFileSnapshot(p)
185+
}
186+
}
187+
87188
if denied := checkToolDirPermission(cfg, t.Name, args); denied != "" {
88189
logAudit(t.Name, auditArgs, "DENIED (dir restriction)")
89190
return denied, nil

cmd/iterate/repl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ func buildCommandContext(repoPath, line string, parts []string, p iteragent.Prov
472472
StreamAndPrint: streamAndPrint,
473473
RunShell: runShell,
474474
PromptLine: selector.PromptLine,
475+
Undo: performUndo,
475476
},
476477
State: commands.StateAccessors{
477478
IsDenied: isDenied,

cmd/iterate/repl_streaming.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ func logTokenDelta(beforeTokens int) {
121121

122122
// streamAndPrint runs the agent and prints the streamed response.
123123
func streamAndPrint(ctx context.Context, a *iteragent.Agent, prompt string, repoPath string) {
124+
beginUndoFrame()
125+
defer commitUndoFrame()
126+
124127
recordMessage()
125128

126129
// Sync pinned messages into the agent before each request.

internal/commands/registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type REPLCallbacks struct {
4646
MakeAgent func() *iteragent.Agent
4747
ReadMultiLine func() (string, bool)
4848
PromptLine func(prompt string) (string, bool)
49+
// Undo reverts the last agent file modifications.
50+
// Returns the list of restored paths and an error (if any).
51+
Undo func() ([]string, error)
4952
}
5053

5154
// StateAccessors groups thread-safe state access callbacks.

internal/commands/utility.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ func registerUtilityActionCommands(r *Registry) {
106106
Category: "utility",
107107
Handler: cmdInject,
108108
})
109+
110+
r.Register(Command{
111+
Name: "/undo",
112+
Aliases: []string{},
113+
Description: "revert last agent file changes",
114+
Category: "utility",
115+
Handler: cmdUndoFiles,
116+
})
109117
}
110118

111119
func cmdContext(ctx Context) Result {
@@ -290,6 +298,30 @@ func cmdFork(ctx Context) Result {
290298
return Result{Handled: true}
291299
}
292300

301+
func cmdUndoFiles(ctx Context) Result {
302+
if ctx.REPL.Undo == nil {
303+
PrintError("undo not available in this session")
304+
return Result{Handled: true}
305+
}
306+
restored, err := ctx.REPL.Undo()
307+
if err != nil {
308+
if len(restored) == 0 {
309+
PrintError("%v", err)
310+
return Result{Handled: true}
311+
}
312+
PrintError("partial undo: %v", err)
313+
}
314+
if len(restored) == 0 {
315+
fmt.Println("Nothing to undo.")
316+
return Result{Handled: true}
317+
}
318+
for _, p := range restored {
319+
fmt.Printf(" %s✓%s restored %s\n", ColorLime, ColorReset, p)
320+
}
321+
PrintSuccess("undone (%d file(s))", len(restored))
322+
return Result{Handled: true}
323+
}
324+
293325
func cmdInject(ctx Context) Result {
294326
text := ctx.Args()
295327
if text == "" {

0 commit comments

Comments
 (0)