mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
Local development snapshot of one session's intertwined workstreams. Committed together because changes span shared files (provider.go, model.go, loop.go, types.go, streamjson) and can't be cleanly split without interactive hunk staging. Build + vet + full -race suite green. Zenline TUI skin: Zen home / Statusline chat / permission-modal surfaces, 5 themes (Phosphor/Cyan/Sage/Violet/Mono), boot splash, mouse support, and a headless --snapshot mode (internal/zenline, cli/zenline.go, tui/zenline_view.go). OpenAI provider: streaming tool-call accumulation hardening plus an idle-timeout watchdog (reader goroutine + context cancel) so a stalled upstream surfaces an error instead of hanging the agent forever. Agent reliability: update_plan schema makes id optional/auto-numbered with an advertised item schema; malformed (nameless) tool calls feed a retry to the model instead of silently ending the turn; secret scrubbing at the registry boundary (covers agent loop AND MCP); structured ToolResult (changedFiles/display/redacted) surfaced via stream-json. Session file checkpoints + safe rewind: before-mutation content snapshots (content-addressed, deduped, size-capped, 0600) captured on write/edit/apply_patch in TUI and headless; RestoreToSequence + ApplyRewind (path-traversal-guarded, session-locked); /rewind in TUI and 'zero sessions rewind' headless; stream-json checkpoint/restore events. Design specs under docs/superpowers/specs/. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/agent"
|
|
"github.com/Gitlawb/zero/internal/sessions"
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
type execSessionRecorder struct {
|
|
prepared sessions.PreparedExec
|
|
err error
|
|
}
|
|
|
|
// captureCheckpoint snapshots the before-state of files a tool call will mutate.
|
|
// Best-effort: failures never affect the run. Returns the checkpoint event and
|
|
// true when one was recorded.
|
|
func (recorder *execSessionRecorder) captureCheckpoint(workspaceRoot string, call agent.ToolCall) (sessions.Event, bool) {
|
|
if recorder.prepared.Store == nil || recorder.prepared.Session.SessionID == "" {
|
|
return sessions.Event{}, false
|
|
}
|
|
var args map[string]any
|
|
if call.Arguments != "" {
|
|
_ = json.Unmarshal([]byte(call.Arguments), &args)
|
|
}
|
|
targets := tools.MutationTargets(workspaceRoot, call.Name, args)
|
|
if len(targets) == 0 {
|
|
return sessions.Event{}, false
|
|
}
|
|
event, err := recorder.prepared.Store.CaptureToolCheckpoint(recorder.prepared.Session.SessionID, workspaceRoot, call.Name, targets)
|
|
if err != nil || event.Type == "" {
|
|
return sessions.Event{}, false
|
|
}
|
|
return event, true
|
|
}
|
|
|
|
func shouldUseExecSession(options execOptions) bool {
|
|
return options.outputFormat == execOutputStreamJSON ||
|
|
options.resume != "" ||
|
|
options.resumeLatest ||
|
|
options.fork != ""
|
|
}
|
|
|
|
func preflightExecSession(options execOptions) error {
|
|
if options.resume == "" && !options.resumeLatest && options.fork == "" {
|
|
return nil
|
|
}
|
|
if (options.resume != "" || options.resumeLatest) && options.fork != "" {
|
|
return execUsageError{"Use either --resume or --fork, not both."}
|
|
}
|
|
|
|
store := sessions.NewStore(sessions.StoreOptions{})
|
|
switch {
|
|
case options.fork != "":
|
|
session, err := store.Get(options.fork)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if session == nil {
|
|
return execUsageError{"Zero session not found: " + options.fork}
|
|
}
|
|
case options.resume != "":
|
|
session, err := store.Get(options.resume)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if session == nil {
|
|
return execUsageError{"Zero session not found: " + options.resume}
|
|
}
|
|
case options.resumeLatest:
|
|
latest, err := store.Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest == nil {
|
|
return execUsageError{"No Zero sessions available to resume."}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createSessionTitle(prompt string) string {
|
|
title := strings.Join(strings.Fields(prompt), " ")
|
|
if len(title) > 80 {
|
|
title = title[:80]
|
|
}
|
|
if title == "" {
|
|
return "Zero exec session"
|
|
}
|
|
return title
|
|
}
|
|
|
|
func (recorder *execSessionRecorder) append(eventType sessions.EventType, payload any) {
|
|
if recorder.err != nil || recorder.prepared.Store == nil || recorder.prepared.Session.SessionID == "" {
|
|
return
|
|
}
|
|
_, recorder.err = recorder.prepared.Store.AppendEvent(recorder.prepared.Session.SessionID, sessions.AppendEventInput{
|
|
Type: eventType,
|
|
Payload: payload,
|
|
})
|
|
}
|