mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-27 13:56:33 +02:00
You see the file being composed in real time now. While the model is typing the tool_use JSON, the TUI renders a rules-wrapped syntax-highlighted preview that grows as deltas arrive. When the tool actually runs, the preview transitions to the final result without flicker. Before: the tool header appeared post-response, then "wrote N bytes" for write / "applied 1 edit" for edit. No live feedback. Now: as soon as the `path` field parses out of the partial JSON, the header shows `▸ write /Users/pat/Desktop/demo.ts`. As the `content` / `newText` string streams in, each delta extends the highlighted preview body immediately. Collapsed at the usual preview height with the standard `ctrl+o to expand` footer. Implementation: - internal/core/events.go: three new AgentEvent types, EvToolUseStart / EvToolUseArgs / EvToolUseEnd. They carry the tool id, name, and raw JSON deltas from the provider stream. - internal/core/agent.go: forwards the equivalent provider events instead of dropping them. EvToolCall (with fully-parsed args) still fires at EventDone as before, so existing consumers don't need to change. - internal/tui/partialjson.go: small escape-aware extractor that pulls one string field's value out of a partial JSON buffer as it grows. Handles \\ \" \n \t \r \b \f \/ and \uXXXX escapes; tolerates trailing incomplete escapes (returns the complete prefix and waits for more bytes). Second helper, ExtractLastNewText, walks to the most recent "newText":"..." inside an edits array so edit's streaming preview shows the edit currently being composed (not an earlier one that's already finished). - internal/tui/view.go: ToolCallView gains Streaming, RawJSONBuf, LivePath fields. renderToolCall dispatches to renderLiveToolBody while Streaming=true and Result=="". For `write` it shows the partial `content`; for `edit` it shows ` edit N (streaming)` plus the partial `newText`. Shared wrapLiveBody keeps the rule + collapse boilerplate in one place. - internal/agent/modes/interactive.go: handles the three new events. EvToolUseStart pre-creates the ToolCallView so the header appears instantly; EvToolUseArgs appends the delta and refreshes LivePath; EvToolUseEnd flips Streaming off. The pre-existing EvToolCall branch now updates the already-created view rather than replacing it. - internal/agent/modes/json.go: emits tool_use_start / tool_use_args / tool_use_end events so `zot --json` consumers can build their own live previews. - internal/agent/tools/write.go: tool result is now the written file body (same shape as read's result) with total_lines + start_line details. Keeps the visual transition from streaming preview to final result seamless, and gives the model the file contents in its own tool_result for follow-up turns. Tests: - internal/tui/partialjson_test.go: 9 cases on ExtractPartialStringField (complete, partial mid-word, escape variants, unfinished escapes) and 4 on ExtractLastNewText (no newText, partial, complete, multi-edit). Verified end-to-end via `zot --json "write ..."` and `zot --json "edit ..."` against the real API: 246 tool_use_args delta events on a 30-line write, preview fields extracted live, final file written correctly.
74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/patriceckhart/zot/internal/core"
|
|
"github.com/patriceckhart/zot/internal/provider"
|
|
)
|
|
|
|
// WriteTool writes content to a file, creating parent directories.
|
|
type WriteTool struct {
|
|
CWD string
|
|
Sandbox *Sandbox
|
|
}
|
|
|
|
type writeArgs struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
const writeSchema = `{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}`
|
|
|
|
func (t *WriteTool) Name() string { return "write" }
|
|
func (t *WriteTool) Description() string {
|
|
return "Write a file. Creates parent dirs. Overwrites."
|
|
}
|
|
func (t *WriteTool) Schema() json.RawMessage { return json.RawMessage(writeSchema) }
|
|
|
|
func (t *WriteTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
|
|
var a writeArgs
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
|
|
}
|
|
if a.Path == "" {
|
|
return core.ToolResult{}, fmt.Errorf("path is required")
|
|
}
|
|
path := resolvePath(t.CWD, a.Path)
|
|
if err := t.Sandbox.CheckPath(path); err != nil {
|
|
return core.ToolResult{}, err
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return core.ToolResult{}, err
|
|
}
|
|
if err := os.WriteFile(path, []byte(a.Content), 0o644); err != nil {
|
|
return core.ToolResult{}, err
|
|
}
|
|
|
|
// Return the file content as the result body, just like `read`
|
|
// does. The TUI renders it with a syntax-highlighted gutter so
|
|
// the on-screen view after a `write` matches the pre-write
|
|
// streaming preview seamlessly. The model also sees the written
|
|
// content in its tool_result, which is useful on follow-up turns
|
|
// where it wants to reference what it just wrote without a
|
|
// second `read` call.
|
|
totalLines := strings.Count(a.Content, "\n")
|
|
if len(a.Content) > 0 && !strings.HasSuffix(a.Content, "\n") {
|
|
totalLines++ // count the last unterminated line
|
|
}
|
|
return core.ToolResult{
|
|
Content: []provider.Content{provider.TextBlock{Text: a.Content}},
|
|
Details: map[string]any{
|
|
"path": path,
|
|
"bytes": len(a.Content),
|
|
"total_lines": totalLines,
|
|
"start_line": 1,
|
|
},
|
|
}, nil
|
|
}
|