Refactor: deal with unused code
This commit is contained in:
@@ -35,7 +35,7 @@ type AgentClient struct {
|
||||
log *slog.Logger
|
||||
chatBody *models.ChatBody
|
||||
sysprompt string
|
||||
lastToolCallID string
|
||||
// lastToolCallID string
|
||||
tools []models.Tool
|
||||
}
|
||||
|
||||
|
||||
76
bot.go
76
bot.go
@@ -58,7 +58,7 @@ var (
|
||||
roleToID = map[string]string{}
|
||||
modelHasVision bool
|
||||
windowToolsAvailable bool
|
||||
tooler *tools.Tools
|
||||
// tooler *tools.Tools
|
||||
//
|
||||
orator Orator
|
||||
asr STT
|
||||
@@ -478,7 +478,7 @@ func UpdateToolCapabilities() {
|
||||
modelHasVision = false
|
||||
if cfg == nil || cfg.CurrentAPI == "" {
|
||||
logger.Warn("cannot determine model capabilities: cfg or CurrentAPI is nil")
|
||||
tooler.RegisterWindowTools(modelHasVision)
|
||||
// tooler.RegisterWindowTools(modelHasVision)
|
||||
return
|
||||
}
|
||||
prevHasVision := modelHasVision
|
||||
@@ -491,7 +491,7 @@ func UpdateToolCapabilities() {
|
||||
showToast("window tools", "Window capture-and-view unavailable: model lacks vision support")
|
||||
}
|
||||
}
|
||||
tooler.RegisterWindowTools(modelHasVision)
|
||||
// tooler.RegisterWindowTools(modelHasVision)
|
||||
}
|
||||
|
||||
// monitorModelLoad starts a goroutine that periodically checks if the specified model is loaded.
|
||||
@@ -1138,7 +1138,7 @@ func findCall(msg, toolCall string) bool {
|
||||
// The ID should come from the streaming response (chunk.ToolID) set earlier.
|
||||
// Some tools like todo_create have "id" in their arguments which is NOT the tool call ID.
|
||||
} else {
|
||||
jsStr := tools.ToolCallRE.FindString(msg)
|
||||
jsStr := models.ToolCallRE.FindString(msg)
|
||||
if jsStr == "" { // no tool call case
|
||||
return false
|
||||
}
|
||||
@@ -1206,17 +1206,42 @@ func findCall(msg, toolCall string) bool {
|
||||
Args: mapToString(lastToolCall.Args),
|
||||
}
|
||||
// call a func
|
||||
_, ok := tools.FnMap[fc.Name]
|
||||
if !ok {
|
||||
m := fc.Name + " is not implemented"
|
||||
// _, ok := tools.FnMap[fc.Name]
|
||||
// if !ok {
|
||||
// m := fc.Name + " is not implemented"
|
||||
// // Create tool response message with the proper tool_call_id
|
||||
// toolResponseMsg := models.RoleMsg{
|
||||
// Role: cfg.ToolRole,
|
||||
// Content: m,
|
||||
// ToolCallID: lastToolCall.ID, // Use the stored tool call ID
|
||||
// }
|
||||
// chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
||||
// logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages))
|
||||
// // Clear the stored tool call ID after using it
|
||||
// lastToolCall.ID = ""
|
||||
// // Trigger the assistant to continue processing with the new tool response
|
||||
// // by calling chatRound with empty content to continue the assistant's response
|
||||
// crr := &models.ChatRoundReq{
|
||||
// Role: cfg.AssistantRole,
|
||||
// }
|
||||
// // failed to find tool
|
||||
// chatRoundChan <- crr
|
||||
// return true
|
||||
// }
|
||||
// Show tool call progress indicator before execution
|
||||
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
||||
toolRunningMode.Store(true)
|
||||
resp, okT := tools.CallToolWithAgent(fc.Name, fc.Args)
|
||||
if !okT {
|
||||
// Create tool response message with the proper tool_call_id
|
||||
toolResponseMsg := models.RoleMsg{
|
||||
Role: cfg.ToolRole,
|
||||
Content: m,
|
||||
Content: string(resp),
|
||||
ToolCallID: lastToolCall.ID, // Use the stored tool call ID
|
||||
}
|
||||
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
||||
logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages))
|
||||
logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role,
|
||||
"content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID)
|
||||
// Clear the stored tool call ID after using it
|
||||
lastToolCall.ID = ""
|
||||
// Trigger the assistant to continue processing with the new tool response
|
||||
@@ -1228,10 +1253,6 @@ func findCall(msg, toolCall string) bool {
|
||||
chatRoundChan <- crr
|
||||
return true
|
||||
}
|
||||
// Show tool call progress indicator before execution
|
||||
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
||||
toolRunningMode.Store(true)
|
||||
resp := tools.CallToolWithAgent(fc.Name, fc.Args)
|
||||
toolRunningMode.Store(false)
|
||||
toolMsg := string(resp)
|
||||
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg)
|
||||
@@ -1289,7 +1310,6 @@ func findCall(msg, toolCall string) bool {
|
||||
fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
|
||||
"\n\n", len(chatBody.Messages), cfg.ToolRole, toolResponseMsg.GetText())
|
||||
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
||||
logger.Debug("findCall: added actual tool response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages))
|
||||
// Clear the stored tool call ID after using it
|
||||
lastToolCall.ID = ""
|
||||
// Trigger the assistant to continue processing with the new tool response
|
||||
@@ -1310,11 +1330,19 @@ func chatToTextSlice(messages []models.RoleMsg, showSys bool) []string {
|
||||
// This is a tool call indicator - show collapsed
|
||||
if toolCollapsed {
|
||||
toolName := messages[i].ToolCall.Name
|
||||
resp[i] = strings.ReplaceAll(fmt.Sprintf("%s\n%s\n[yellow::i][tool call: %s (press Ctrl+T to expand)][-:-:-]\n", icon, messages[i].GetText(), toolName), "\n\n", "\n")
|
||||
resp[i] = strings.ReplaceAll(
|
||||
fmt.Sprintf(
|
||||
"%s\n%s\n[yellow::i][tool call: %s (press Ctrl+T to expand)][-:-:-]\n",
|
||||
icon, messages[i].GetText(), toolName),
|
||||
"\n\n", "\n")
|
||||
} else {
|
||||
// Show full tool call info
|
||||
toolName := messages[i].ToolCall.Name
|
||||
resp[i] = strings.ReplaceAll(fmt.Sprintf("%s\n%s\n[yellow::i][tool call: %s][-:-:-]\nargs: %s\nid: %s\n", icon, messages[i].GetText(), toolName, messages[i].ToolCall.Args, messages[i].ToolCall.ID), "\n\n", "\n")
|
||||
resp[i] = strings.ReplaceAll(
|
||||
fmt.Sprintf(
|
||||
"%s\n%s\n[yellow::i][tool call: %s][-:-:-]\nargs: %s\nid: %s\n",
|
||||
icon, messages[i].GetText(), toolName, messages[i].ToolCall.Args, messages[i].ToolCall.ID),
|
||||
"\n\n", "\n")
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -1348,7 +1376,7 @@ func chatToText(messages []models.RoleMsg, showSys bool) string {
|
||||
text := strings.Join(s, "\n")
|
||||
// Collapse thinking blocks if enabled
|
||||
if thinkingCollapsed {
|
||||
text = tools.ThinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
text = models.ThinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
// Extract content between <think> and </think>
|
||||
start := len("<think>")
|
||||
end := len(match) - len("</think>")
|
||||
@@ -1364,7 +1392,9 @@ func chatToText(messages []models.RoleMsg, showSys bool) string {
|
||||
startIdx := strings.Index(text, "<think>")
|
||||
if startIdx != -1 {
|
||||
content := text[startIdx+len("<think>"):]
|
||||
placeholder := fmt.Sprintf("[yellow::i][thinking... (%d chars) (press Alt+T to expand)][-:-:-]", len(content))
|
||||
placeholder := fmt.Sprintf(
|
||||
"[yellow::i][thinking... (%d chars) (press Alt+T to expand)][-:-:-]",
|
||||
len(content))
|
||||
text = text[:startIdx] + placeholder
|
||||
}
|
||||
}
|
||||
@@ -1476,8 +1506,11 @@ func summarizeAndStartNewChat() {
|
||||
return
|
||||
}
|
||||
showToast("info", "Summarizing chat history...")
|
||||
arg := map[string]string{
|
||||
"chat": chatToText(chatBody.Messages, false),
|
||||
}
|
||||
// Call the summarize_chat tool via agent
|
||||
summaryBytes := tools.CallToolWithAgent("summarize_chat", map[string]string{})
|
||||
summaryBytes, _ := tools.CallToolWithAgent("summarize_chat", arg)
|
||||
summary := string(summaryBytes)
|
||||
if summary == "" {
|
||||
showToast("error", "Failed to generate summary")
|
||||
@@ -1588,6 +1621,7 @@ func init() {
|
||||
cachedModelColor.Store("orange")
|
||||
go chatWatcher(ctx)
|
||||
initTUI()
|
||||
tooler = tools.InitTools(cfg, logger, store)
|
||||
tooler.RegisterWindowTools(modelHasVision)
|
||||
tools.InitTools(cfg, logger, store)
|
||||
// tooler = tools.InitTools(cfg, logger, store)
|
||||
// tooler.RegisterWindowTools(modelHasVision)
|
||||
}
|
||||
|
||||
17
helpfuncs.go
17
helpfuncs.go
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"gf-lt/models"
|
||||
"gf-lt/pngmeta"
|
||||
"gf-lt/tools"
|
||||
"image"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -87,8 +86,8 @@ func stripThinkingFromMsg(msg *models.RoleMsg) *models.RoleMsg {
|
||||
}
|
||||
// Strip thinking from assistant messages
|
||||
msgText := msg.GetText()
|
||||
if tools.ThinkRE.MatchString(msgText) {
|
||||
cleanedText := tools.ThinkRE.ReplaceAllString(msgText, "")
|
||||
if models.ThinkRE.MatchString(msgText) {
|
||||
cleanedText := models.ThinkRE.ReplaceAllString(msgText, "")
|
||||
cleanedText = strings.TrimSpace(cleanedText)
|
||||
msg.SetText(cleanedText)
|
||||
}
|
||||
@@ -149,7 +148,7 @@ func colorText() {
|
||||
placeholderThink := "__THINK_BLOCK_%d__"
|
||||
counterThink := 0
|
||||
// Replace code blocks with placeholders and store their styled versions
|
||||
text = tools.CodeBlockRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
text = models.CodeBlockRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
// Style the code block and store it
|
||||
styled := fmt.Sprintf("[red::i]%s[-:-:-]", match)
|
||||
codeBlocks = append(codeBlocks, styled)
|
||||
@@ -158,7 +157,7 @@ func colorText() {
|
||||
counter++
|
||||
return id
|
||||
})
|
||||
text = tools.ThinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
text = models.ThinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
// Style the code block and store it
|
||||
styled := fmt.Sprintf("[red::i]%s[-:-:-]", match)
|
||||
thinkBlocks = append(thinkBlocks, styled)
|
||||
@@ -168,9 +167,9 @@ func colorText() {
|
||||
return id
|
||||
})
|
||||
// Step 2: Apply other regex styles to the non-code parts
|
||||
text = tools.QuotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`)
|
||||
text = tools.StarRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||
text = tools.SingleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`")
|
||||
text = models.QuotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`)
|
||||
text = models.StarRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||
text = models.SingleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`")
|
||||
// text = tools.ThinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`)
|
||||
// Step 3: Restore the styled code blocks from placeholders
|
||||
for i, cb := range codeBlocks {
|
||||
@@ -189,7 +188,7 @@ func updateStatusLine() {
|
||||
|
||||
func initSysCards() ([]string, error) {
|
||||
labels := []string{}
|
||||
labels = append(labels, tools.SysLabels...)
|
||||
labels = append(labels, models.SysLabels...)
|
||||
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger)
|
||||
if err != nil {
|
||||
logger.Error("failed to read sys dir", "error", err)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package models
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
LoadedMark = "(loaded) "
|
||||
ToolRespMultyType = "multimodel_content"
|
||||
@@ -13,3 +15,17 @@ const (
|
||||
APITypeChat APIType = iota
|
||||
APITypeCompletion
|
||||
)
|
||||
|
||||
var (
|
||||
ToolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
|
||||
QuotesRE = regexp.MustCompile(`(".*?")`)
|
||||
StarRE = regexp.MustCompile(`(\*.*?\*)`)
|
||||
ThinkRE = regexp.MustCompile(`(?s)<think>.*?</think>`)
|
||||
CodeBlockRE = regexp.MustCompile(`(?s)\x60{3}(?:.*?)\n(.*?)\n\s*\x60{3}\s*`)
|
||||
SingleBacktickRE = regexp.MustCompile(`\x60([^\x60]*)\x60`)
|
||||
RoleRE = regexp.MustCompile(`^(\w+):`)
|
||||
)
|
||||
|
||||
var (
|
||||
SysLabels = []string{"assistant"}
|
||||
)
|
||||
|
||||
@@ -32,10 +32,8 @@ func ParseChain(input string) []Segment {
|
||||
var current strings.Builder
|
||||
runes := []rune(input)
|
||||
n := len(runes)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
ch := runes[i]
|
||||
|
||||
// handle quotes
|
||||
if ch == '\'' || ch == '"' {
|
||||
quote := ch
|
||||
@@ -50,7 +48,6 @@ func ParseChain(input string) []Segment {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// &&
|
||||
if ch == '&' && i+1 < n && runes[i+1] == '&' {
|
||||
segments = append(segments, Segment{
|
||||
@@ -61,7 +58,6 @@ func ParseChain(input string) []Segment {
|
||||
i++ // skip second &
|
||||
continue
|
||||
}
|
||||
|
||||
// ;
|
||||
if ch == ';' {
|
||||
segments = append(segments, Segment{
|
||||
@@ -71,7 +67,6 @@ func ParseChain(input string) []Segment {
|
||||
current.Reset()
|
||||
continue
|
||||
}
|
||||
|
||||
// ||
|
||||
if ch == '|' && i+1 < n && runes[i+1] == '|' {
|
||||
segments = append(segments, Segment{
|
||||
@@ -82,7 +77,6 @@ func ParseChain(input string) []Segment {
|
||||
i++ // skip second |
|
||||
continue
|
||||
}
|
||||
|
||||
// | (single pipe)
|
||||
if ch == '|' {
|
||||
segments = append(segments, Segment{
|
||||
@@ -92,16 +86,13 @@ func ParseChain(input string) []Segment {
|
||||
current.Reset()
|
||||
continue
|
||||
}
|
||||
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
|
||||
// last segment
|
||||
last := strings.TrimSpace(current.String())
|
||||
if last != "" {
|
||||
segments = append(segments, Segment{Raw: last, Op: OpNone})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
@@ -112,12 +103,10 @@ func ExecChain(command string) string {
|
||||
if len(segments) == 0 {
|
||||
return "[error] empty command"
|
||||
}
|
||||
|
||||
var collected []string
|
||||
var lastOutput string
|
||||
var lastErr error
|
||||
pipeInput := ""
|
||||
|
||||
for i, seg := range segments {
|
||||
if i > 0 {
|
||||
prevOp := segments[i-1].Op
|
||||
@@ -130,7 +119,6 @@ func ExecChain(command string) string {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// determine stdin for this segment
|
||||
segStdin := ""
|
||||
if i == 0 {
|
||||
@@ -138,9 +126,7 @@ func ExecChain(command string) string {
|
||||
} else if segments[i-1].Op == OpPipe {
|
||||
segStdin = lastOutput
|
||||
}
|
||||
|
||||
lastOutput, lastErr = execSingle(seg.Raw, segStdin)
|
||||
|
||||
// pipe: output flows to next command's stdin
|
||||
// && or ;: collect output
|
||||
if i < len(segments)-1 && seg.Op == OpPipe {
|
||||
@@ -150,7 +136,6 @@ func ExecChain(command string) string {
|
||||
collected = append(collected, lastOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(collected, "\n")
|
||||
}
|
||||
|
||||
@@ -160,15 +145,12 @@ func execSingle(command, stdin string) (string, error) {
|
||||
if len(parts) == 0 {
|
||||
return "", fmt.Errorf("empty command")
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
// Check if it's a built-in Go command
|
||||
if result := execBuiltin(name, args, stdin); result != "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Otherwise execute as system command
|
||||
cmd := exec.Command(name, args...)
|
||||
if stdin != "" {
|
||||
@@ -187,7 +169,6 @@ func tokenize(input string) []string {
|
||||
var current strings.Builder
|
||||
inQuote := false
|
||||
var quoteChar rune
|
||||
|
||||
for _, ch := range input {
|
||||
if inQuote {
|
||||
if ch == quoteChar {
|
||||
@@ -197,13 +178,11 @@ func tokenize(input string) []string {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '\'' || ch == '"' {
|
||||
inQuote = true
|
||||
quoteChar = ch
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == ' ' || ch == '\t' {
|
||||
if current.Len() > 0 {
|
||||
tokens = append(tokens, current.String())
|
||||
@@ -211,14 +190,11 @@ func tokenize(input string) []string {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
tokens = append(tokens, current.String())
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
@@ -242,7 +218,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
path := args[0]
|
||||
abs := path
|
||||
if !filepath.IsAbs(path) {
|
||||
abs = filepath.Join(fsRootDir, path)
|
||||
abs = filepath.Join(cfg.FilePickerDir, path)
|
||||
}
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
@@ -250,16 +226,16 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
}
|
||||
return string(data)
|
||||
case "pwd":
|
||||
return fsRootDir
|
||||
return cfg.FilePickerDir
|
||||
case "cd":
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: cd <dir>"
|
||||
}
|
||||
dir := args[0]
|
||||
// Resolve relative to fsRootDir
|
||||
// Resolve relative to cfg.FilePickerDir
|
||||
abs := dir
|
||||
if !filepath.IsAbs(dir) {
|
||||
abs = filepath.Join(fsRootDir, dir)
|
||||
abs = filepath.Join(cfg.FilePickerDir, dir)
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
info, err := os.Stat(abs)
|
||||
@@ -269,8 +245,8 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
if !info.IsDir() {
|
||||
return fmt.Sprintf("[error] cd: not a directory: %s", dir)
|
||||
}
|
||||
fsRootDir = abs
|
||||
return fmt.Sprintf("Changed directory to: %s", fsRootDir)
|
||||
cfg.FilePickerDir = abs
|
||||
return fmt.Sprintf("Changed directory to: %s", cfg.FilePickerDir)
|
||||
case "mkdir":
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: mkdir [-p] <dir>"
|
||||
@@ -289,7 +265,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
}
|
||||
abs := dirPath
|
||||
if !filepath.IsAbs(dirPath) {
|
||||
abs = filepath.Join(fsRootDir, dirPath)
|
||||
abs = filepath.Join(cfg.FilePickerDir, dirPath)
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
var mkdirFunc func(string, os.FileMode) error
|
||||
@@ -315,7 +291,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
}
|
||||
abs := dir
|
||||
if !filepath.IsAbs(dir) {
|
||||
abs = filepath.Join(fsRootDir, dir)
|
||||
abs = filepath.Join(cfg.FilePickerDir, dir)
|
||||
}
|
||||
entries, err := os.ReadDir(abs)
|
||||
if err != nil {
|
||||
@@ -347,7 +323,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
||||
return "[error] usage: go <subcommand> [options]"
|
||||
}
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Dir = fsRootDir
|
||||
cmd.Dir = cfg.FilePickerDir
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] go %s: %v\n%s", args[0], err, string(output))
|
||||
|
||||
82
tools/fs.go
82
tools/fs.go
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var fsRootDir string
|
||||
var memoryStore MemoryStore
|
||||
var agentRole string
|
||||
|
||||
@@ -31,11 +30,11 @@ func SetMemoryStore(store MemoryStore, role string) {
|
||||
}
|
||||
|
||||
func SetFSRoot(dir string) {
|
||||
fsRootDir = dir
|
||||
cfg.FilePickerDir = dir
|
||||
}
|
||||
|
||||
func GetFSRoot() string {
|
||||
return fsRootDir
|
||||
return cfg.FilePickerDir
|
||||
}
|
||||
|
||||
func SetFSCwd(dir string) error {
|
||||
@@ -50,26 +49,24 @@ func SetFSCwd(dir string) error {
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("not a directory: %s", dir)
|
||||
}
|
||||
fsRootDir = abs
|
||||
cfg.FilePickerDir = abs
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolvePath(rel string) (string, error) {
|
||||
if fsRootDir == "" {
|
||||
if cfg.FilePickerDir == "" {
|
||||
return "", fmt.Errorf("fs root not set")
|
||||
}
|
||||
|
||||
if filepath.IsAbs(rel) {
|
||||
abs := filepath.Clean(rel)
|
||||
if !strings.HasPrefix(abs, fsRootDir+string(os.PathSeparator)) && abs != fsRootDir {
|
||||
if !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
||||
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
abs := filepath.Join(fsRootDir, rel)
|
||||
abs := filepath.Join(cfg.FilePickerDir, rel)
|
||||
abs = filepath.Clean(abs)
|
||||
if !strings.HasPrefix(abs, fsRootDir+string(os.PathSeparator)) && abs != fsRootDir {
|
||||
if !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
||||
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
||||
}
|
||||
return abs, nil
|
||||
@@ -100,12 +97,10 @@ func FsLs(args []string, stdin string) string {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(abs)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] ls: %v", err)
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
for _, e := range entries {
|
||||
info, _ := e.Info()
|
||||
@@ -136,17 +131,14 @@ func FsCat(args []string, stdin string) string {
|
||||
if path == "" {
|
||||
return "[error] usage: cat <path>"
|
||||
}
|
||||
|
||||
abs, err := resolvePath(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] cat: %v", err)
|
||||
}
|
||||
|
||||
if b64 {
|
||||
result := base64.StdEncoding.EncodeToString(data)
|
||||
if IsImageFile(path) {
|
||||
@@ -162,7 +154,6 @@ func FsViewImg(args []string, stdin string) string {
|
||||
return "[error] usage: view_img <image-path>"
|
||||
}
|
||||
path := args[0]
|
||||
|
||||
var abs string
|
||||
if filepath.IsAbs(path) {
|
||||
abs = path
|
||||
@@ -173,20 +164,16 @@ func FsViewImg(args []string, stdin string) string {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(abs); err != nil {
|
||||
return fmt.Sprintf("[error] view_img: %v", err)
|
||||
}
|
||||
|
||||
if !IsImageFile(path) {
|
||||
return fmt.Sprintf("[error] not an image file: %s (use cat to read text files)", path)
|
||||
}
|
||||
|
||||
dataURL, err := models.CreateImageURLFromPath(abs)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] view_img: %v", err)
|
||||
}
|
||||
|
||||
result := models.MultimodalToolResp{
|
||||
Type: "multimodal_content",
|
||||
Parts: []map[string]string{
|
||||
@@ -222,16 +209,13 @@ func FsWrite(args []string, stdin string) string {
|
||||
if path == "" {
|
||||
return "[error] usage: write <path> [content] or pipe stdin"
|
||||
}
|
||||
|
||||
abs, err := resolvePath(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
|
||||
return fmt.Sprintf("[error] mkdir: %v", err)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if b64 {
|
||||
src := stdin
|
||||
@@ -251,18 +235,14 @@ func FsWrite(args []string, stdin string) string {
|
||||
data = []byte(stdin)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(abs, data, 0o644); err != nil {
|
||||
return fmt.Sprintf("[error] write: %v", err)
|
||||
}
|
||||
|
||||
size := humanSize(int64(len(data)))
|
||||
result := fmt.Sprintf("Written %s → %s", size, path)
|
||||
|
||||
if IsImageFile(path) {
|
||||
result += fmt.Sprintf("\n", abs)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -270,17 +250,14 @@ func FsStat(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: stat <path>"
|
||||
}
|
||||
|
||||
abs, err := resolvePath(args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] stat: %v", err)
|
||||
}
|
||||
|
||||
mime := "application/octet-stream"
|
||||
if IsImageFile(args[0]) {
|
||||
ext := strings.ToLower(filepath.Ext(args[0]))
|
||||
@@ -297,7 +274,6 @@ func FsStat(args []string, stdin string) string {
|
||||
mime = "image/svg+xml"
|
||||
}
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
fmt.Fprintf(&out, "File: %s\n", args[0])
|
||||
fmt.Fprintf(&out, "Size: %s (%d bytes)\n", humanSize(info.Size()), info.Size())
|
||||
@@ -313,12 +289,10 @@ func FsRm(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: rm <path>"
|
||||
}
|
||||
|
||||
abs, err := resolvePath(args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(abs); err != nil {
|
||||
return fmt.Sprintf("[error] rm: %v", err)
|
||||
}
|
||||
@@ -329,7 +303,6 @@ func FsCp(args []string, stdin string) string {
|
||||
if len(args) < 2 {
|
||||
return "[error] usage: cp <src> <dst>"
|
||||
}
|
||||
|
||||
srcAbs, err := resolvePath(args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
@@ -338,16 +311,13 @@ func FsCp(args []string, stdin string) string {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(srcAbs)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] cp read: %v", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
||||
return fmt.Sprintf("[error] cp mkdir: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(dstAbs, data, 0o644); err != nil {
|
||||
return fmt.Sprintf("[error] cp write: %v", err)
|
||||
}
|
||||
@@ -358,7 +328,6 @@ func FsMv(args []string, stdin string) string {
|
||||
if len(args) < 2 {
|
||||
return "[error] usage: mv <src> <dst>"
|
||||
}
|
||||
|
||||
srcAbs, err := resolvePath(args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
@@ -367,11 +336,9 @@ func FsMv(args []string, stdin string) string {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
||||
return fmt.Sprintf("[error] mv mkdir: %v", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(srcAbs, dstAbs); err != nil {
|
||||
return fmt.Sprintf("[error] mv: %v", err)
|
||||
}
|
||||
@@ -382,10 +349,8 @@ func FsMkdir(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: mkdir [-p] <dir>"
|
||||
}
|
||||
|
||||
createParents := false
|
||||
var dirPath string
|
||||
|
||||
for _, a := range args {
|
||||
if a == "-p" || a == "--parents" {
|
||||
createParents = true
|
||||
@@ -393,27 +358,22 @@ func FsMkdir(args []string, stdin string) string {
|
||||
dirPath = a
|
||||
}
|
||||
}
|
||||
|
||||
if dirPath == "" {
|
||||
return "[error] usage: mkdir [-p] <dir>"
|
||||
}
|
||||
|
||||
abs, err := resolvePath(dirPath)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] %v", err)
|
||||
}
|
||||
|
||||
var mkdirFunc func(string, os.FileMode) error
|
||||
if createParents {
|
||||
mkdirFunc = os.MkdirAll
|
||||
} else {
|
||||
mkdirFunc = os.Mkdir
|
||||
}
|
||||
|
||||
if err := mkdirFunc(abs, 0o755); err != nil {
|
||||
return fmt.Sprintf("[error] mkdir: %v", err)
|
||||
}
|
||||
|
||||
if createParents {
|
||||
return fmt.Sprintf("Created %s (with parents)", dirPath)
|
||||
}
|
||||
@@ -459,7 +419,6 @@ func FsGrep(args []string, stdin string) string {
|
||||
if ignoreCase {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
|
||||
lines := strings.Split(stdin, "\n")
|
||||
var matched []string
|
||||
for _, line := range lines {
|
||||
@@ -549,7 +508,6 @@ func FsSort(args []string, stdin string) string {
|
||||
numeric = true
|
||||
}
|
||||
}
|
||||
|
||||
sortFunc := func(i, j int) bool {
|
||||
if numeric {
|
||||
ni, _ := strconv.Atoi(lines[i])
|
||||
@@ -564,7 +522,6 @@ func FsSort(args []string, stdin string) string {
|
||||
}
|
||||
return lines[i] < lines[j]
|
||||
}
|
||||
|
||||
sort.Slice(lines, sortFunc)
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -577,7 +534,6 @@ func FsUniq(args []string, stdin string) string {
|
||||
showCount = true
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
var prev string
|
||||
first := true
|
||||
@@ -623,17 +579,14 @@ func FsGit(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: git <subcommand> [options]"
|
||||
}
|
||||
|
||||
subcmd := args[0]
|
||||
if !allowedGitSubcommands[subcmd] {
|
||||
return fmt.Sprintf("[error] git: '%s' is not an allowed git command. Allowed: status, log, diff, show, branch, reflog, rev-parse, shortlog, describe, rev-list", subcmd)
|
||||
}
|
||||
|
||||
abs, err := resolvePath(".")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[error] git: %v", err)
|
||||
}
|
||||
|
||||
// Pass all args to git (first arg is subcommand, rest are options)
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = abs
|
||||
@@ -645,7 +598,7 @@ func FsGit(args []string, stdin string) string {
|
||||
}
|
||||
|
||||
func FsPwd(args []string, stdin string) string {
|
||||
return fsRootDir
|
||||
return cfg.FilePickerDir
|
||||
}
|
||||
|
||||
func FsCd(args []string, stdin string) string {
|
||||
@@ -664,19 +617,17 @@ func FsCd(args []string, stdin string) string {
|
||||
if !info.IsDir() {
|
||||
return fmt.Sprintf("[error] cd: not a directory: %s", dir)
|
||||
}
|
||||
fsRootDir = abs
|
||||
return fmt.Sprintf("Changed directory to: %s", fsRootDir)
|
||||
cfg.FilePickerDir = abs
|
||||
return fmt.Sprintf("Changed directory to: %s", cfg.FilePickerDir)
|
||||
}
|
||||
|
||||
func FsSed(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: sed 's/old/new/[g]' [file]"
|
||||
}
|
||||
|
||||
inPlace := false
|
||||
var filePath string
|
||||
var pattern string
|
||||
|
||||
for _, a := range args {
|
||||
if a == "-i" || a == "--in-place" {
|
||||
inPlace = true
|
||||
@@ -687,21 +638,17 @@ func FsSed(args []string, stdin string) string {
|
||||
filePath = a
|
||||
}
|
||||
}
|
||||
|
||||
if pattern == "" {
|
||||
return "[error] usage: sed 's/old/new/[g]' [file]"
|
||||
}
|
||||
|
||||
// Parse pattern: s/old/new/flags
|
||||
parts := strings.Split(pattern[1:], "/")
|
||||
if len(parts) < 2 {
|
||||
return "[error] invalid sed pattern. Use: s/old/new/[g]"
|
||||
}
|
||||
|
||||
oldStr := parts[0]
|
||||
newStr := parts[1]
|
||||
global := len(parts) >= 3 && strings.Contains(parts[2], "g")
|
||||
|
||||
var content string
|
||||
if filePath != "" && stdin == "" {
|
||||
// Read from file
|
||||
@@ -720,14 +667,12 @@ func FsSed(args []string, stdin string) string {
|
||||
} else {
|
||||
return "[error] sed: no input (use file path or pipe from stdin)"
|
||||
}
|
||||
|
||||
// Apply sed replacement
|
||||
if global {
|
||||
content = strings.ReplaceAll(content, oldStr, newStr)
|
||||
} else {
|
||||
content = strings.Replace(content, oldStr, newStr, 1)
|
||||
}
|
||||
|
||||
if inPlace && filePath != "" {
|
||||
abs, err := resolvePath(filePath)
|
||||
if err != nil {
|
||||
@@ -738,7 +683,6 @@ func FsSed(args []string, stdin string) string {
|
||||
}
|
||||
return fmt.Sprintf("Modified %s", filePath)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -746,11 +690,9 @@ func FsMemory(args []string, stdin string) string {
|
||||
if len(args) == 0 {
|
||||
return "[error] usage: memory store <topic> <data> | memory get <topic> | memory list | memory forget <topic>"
|
||||
}
|
||||
|
||||
if memoryStore == nil {
|
||||
return "[error] memory store not initialized"
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "store":
|
||||
if len(args) < 3 && stdin == "" {
|
||||
@@ -768,7 +710,6 @@ func FsMemory(args []string, stdin string) string {
|
||||
return fmt.Sprintf("[error] failed to store: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Stored under topic: %s", topic)
|
||||
|
||||
case "get":
|
||||
if len(args) < 2 {
|
||||
return "[error] usage: memory get <topic>"
|
||||
@@ -779,7 +720,6 @@ func FsMemory(args []string, stdin string) string {
|
||||
return fmt.Sprintf("[error] failed to recall: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Topic: %s\n%s", topic, data)
|
||||
|
||||
case "list", "topics":
|
||||
topics, err := memoryStore.RecallTopics(agentRole)
|
||||
if err != nil {
|
||||
@@ -789,7 +729,6 @@ func FsMemory(args []string, stdin string) string {
|
||||
return "No topics stored."
|
||||
}
|
||||
return "Topics: " + strings.Join(topics, ", ")
|
||||
|
||||
case "forget", "delete":
|
||||
if len(args) < 2 {
|
||||
return "[error] usage: memory forget <topic>"
|
||||
@@ -800,7 +739,6 @@ func FsMemory(args []string, stdin string) string {
|
||||
return fmt.Sprintf("[error] failed to forget: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Deleted topic: %s", topic)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("[error] unknown subcommand: %s. Use: store, get, list, topics, forget, delete", args[0])
|
||||
}
|
||||
|
||||
83
tools/pw.go
83
tools/pw.go
@@ -12,87 +12,6 @@ import (
|
||||
"github.com/playwright-community/playwright-go"
|
||||
)
|
||||
|
||||
var browserToolSysMsg = `
|
||||
Additional browser automation tools (Playwright):
|
||||
[
|
||||
{
|
||||
"name": "pw_start",
|
||||
"args": [],
|
||||
"when_to_use": "start a browser instance before doing any browser automation. Must be called first."
|
||||
},
|
||||
{
|
||||
"name": "pw_stop",
|
||||
"args": [],
|
||||
"when_to_use": "stop the browser instance when done with automation."
|
||||
},
|
||||
{
|
||||
"name": "pw_is_running",
|
||||
"args": [],
|
||||
"when_to_use": "check if browser is currently running."
|
||||
},
|
||||
{
|
||||
"name": "pw_navigate",
|
||||
"args": ["url"],
|
||||
"when_to_use": "open a specific URL in the web browser."
|
||||
},
|
||||
{
|
||||
"name": "pw_click",
|
||||
"args": ["selector", "index"],
|
||||
"when_to_use": "click on an element on the current webpage. Use 'index' for multiple matches (default 0)."
|
||||
},
|
||||
{
|
||||
"name": "pw_fill",
|
||||
"args": ["selector", "text", "index"],
|
||||
"when_to_use": "type text into an input field. Use 'index' for multiple matches (default 0)."
|
||||
},
|
||||
{
|
||||
"name": "pw_extract_text",
|
||||
"args": ["selector"],
|
||||
"when_to_use": "extract text content from the page or specific elements. Use selector 'body' for all page text."
|
||||
},
|
||||
{
|
||||
"name": "pw_screenshot",
|
||||
"args": ["selector", "full_page"],
|
||||
"when_to_use": "take a screenshot of the page or a specific element. Returns a file path to the image. Use to verify actions or inspect visual state."
|
||||
},
|
||||
{
|
||||
"name": "pw_screenshot_and_view",
|
||||
"args": ["selector", "full_page"],
|
||||
"when_to_use": "take a screenshot and return the image for viewing. Use to visually verify page state."
|
||||
},
|
||||
{
|
||||
"name": "pw_wait_for_selector",
|
||||
"args": ["selector", "timeout"],
|
||||
"when_to_use": "wait for an element to appear on the page before proceeding with further actions."
|
||||
},
|
||||
{
|
||||
"name": "pw_drag",
|
||||
"args": ["x1", "y1", "x2", "y2"],
|
||||
"when_to_use": "drag the mouse from point (x1,y1) to (x2,y2)."
|
||||
},
|
||||
{
|
||||
"name": "pw_click_at",
|
||||
"args": ["x", "y"],
|
||||
"when_to_use": "click at specific X,Y coordinates on the page. Use when you know the exact position."
|
||||
},
|
||||
{
|
||||
"name": "pw_get_html",
|
||||
"args": ["selector"],
|
||||
"when_to_use": "get the HTML content of the page or a specific element. Use to understand page structure or extract raw HTML."
|
||||
},
|
||||
{
|
||||
"name": "pw_get_dom",
|
||||
"args": ["selector"],
|
||||
"when_to_use": "get a structured DOM representation with tag, attributes, text, and children. Use to inspect element hierarchy and properties."
|
||||
},
|
||||
{
|
||||
"name": "pw_search_elements",
|
||||
"args": ["text", "selector"],
|
||||
"when_to_use": "search for elements by text content or CSS selector. Returns matching elements with their tags, text, and HTML."
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
var (
|
||||
pw *playwright.Playwright
|
||||
browser playwright.Browser
|
||||
@@ -532,6 +451,7 @@ func pwDragBySelector(args map[string]string) []byte {
|
||||
return []byte(fmt.Sprintf(`{"success": true, "message": "%s"}`, msg))
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
func pwClickAt(args map[string]string) []byte {
|
||||
x, ok := args["x"]
|
||||
if !ok {
|
||||
@@ -682,6 +602,7 @@ func pwGetDOM(args map[string]string) []byte {
|
||||
return []byte(fmt.Sprintf(`{"dom": %s}`, string(data)))
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
func pwSearchElements(args map[string]string) []byte {
|
||||
text := args["text"]
|
||||
selector := args["selector"]
|
||||
|
||||
1185
tools/tools.go
1185
tools/tools.go
File diff suppressed because it is too large
Load Diff
4
tui.go
4
tui.go
@@ -850,7 +850,7 @@ func initTUI() {
|
||||
if event.Key() == tcell.KeyF9 {
|
||||
// table of codeblocks to copy
|
||||
text := textView.GetText(false)
|
||||
cb := tools.CodeBlockRE.FindAllString(text, -1)
|
||||
cb := models.CodeBlockRE.FindAllString(text, -1)
|
||||
if len(cb) == 0 {
|
||||
showToast("notify", "no code blocks in chat")
|
||||
return nil
|
||||
@@ -1147,7 +1147,7 @@ func initTUI() {
|
||||
}
|
||||
// check if plain text
|
||||
if !injectRole {
|
||||
matches := tools.RoleRE.FindStringSubmatch(msgText)
|
||||
matches := models.RoleRE.FindStringSubmatch(msgText)
|
||||
if len(matches) > 1 {
|
||||
persona = matches[1]
|
||||
msgText = strings.TrimLeft(msgText[len(matches[0]):], " ")
|
||||
|
||||
Reference in New Issue
Block a user