Merge branch 'master' into feat/agent-flow
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
*.txt
|
|
||||||
*.json
|
*.json
|
||||||
testlog
|
testlog
|
||||||
history/
|
history/
|
||||||
@@ -18,3 +17,4 @@ chat_exports/*.json
|
|||||||
ragimport
|
ragimport
|
||||||
.env
|
.env
|
||||||
onnx/
|
onnx/
|
||||||
|
*.log
|
||||||
|
|||||||
152
bot.go
152
bot.go
@@ -25,6 +25,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -46,7 +48,63 @@ var (
|
|||||||
chunkParser ChunkParser
|
chunkParser ChunkParser
|
||||||
lastToolCall *models.FuncCall
|
lastToolCall *models.FuncCall
|
||||||
lastRespStats *models.ResponseStats
|
lastRespStats *models.ResponseStats
|
||||||
//nolint:unused // TTS_ENABLED conditionally uses this
|
|
||||||
|
outputHandler OutputHandler
|
||||||
|
cliPrevOutput string
|
||||||
|
cliRespDone chan bool
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutputHandler interface {
|
||||||
|
Write(p string)
|
||||||
|
Writef(format string, args ...interface{})
|
||||||
|
ScrollToEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TUIOutputHandler struct {
|
||||||
|
tv *tview.TextView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TUIOutputHandler) Write(p string) {
|
||||||
|
if h.tv != nil {
|
||||||
|
fmt.Fprint(h.tv, p)
|
||||||
|
}
|
||||||
|
if cfg != nil && cfg.CLIMode {
|
||||||
|
fmt.Print(p)
|
||||||
|
cliPrevOutput = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TUIOutputHandler) Writef(format string, args ...interface{}) {
|
||||||
|
s := fmt.Sprintf(format, args...)
|
||||||
|
if h.tv != nil {
|
||||||
|
fmt.Fprint(h.tv, s)
|
||||||
|
}
|
||||||
|
if cfg != nil && cfg.CLIMode {
|
||||||
|
fmt.Print(s)
|
||||||
|
cliPrevOutput = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TUIOutputHandler) ScrollToEnd() {
|
||||||
|
if h.tv != nil {
|
||||||
|
h.tv.ScrollToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CLIOutputHandler struct{}
|
||||||
|
|
||||||
|
func (h *CLIOutputHandler) Write(p string) {
|
||||||
|
fmt.Print(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CLIOutputHandler) Writef(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CLIOutputHandler) ScrollToEnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
basicCard = &models.CharCard{
|
basicCard = &models.CharCard{
|
||||||
ID: models.ComputeCardID("assistant", "basic_sys"),
|
ID: models.ComputeCardID("assistant", "basic_sys"),
|
||||||
SysPrompt: models.BasicSysMsg,
|
SysPrompt: models.BasicSysMsg,
|
||||||
@@ -800,6 +858,10 @@ func chatWatcher(ctx context.Context) {
|
|||||||
|
|
||||||
// inpired by https://github.com/rivo/tview/issues/225
|
// inpired by https://github.com/rivo/tview/issues/225
|
||||||
func showSpinner() {
|
func showSpinner() {
|
||||||
|
if cfg.CLIMode {
|
||||||
|
showSpinnerCLI()
|
||||||
|
return
|
||||||
|
}
|
||||||
spinners := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
spinners := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||||
var i int
|
var i int
|
||||||
botPersona := cfg.AssistantRole
|
botPersona := cfg.AssistantRole
|
||||||
@@ -826,6 +888,12 @@ func showSpinner() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showSpinnerCLI() {
|
||||||
|
for botRespMode.Load() || toolRunningMode.Load() {
|
||||||
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func chatRound(r *models.ChatRoundReq) error {
|
func chatRound(r *models.ChatRoundReq) error {
|
||||||
interruptResp.Store(false)
|
interruptResp.Store(false)
|
||||||
botRespMode.Store(true)
|
botRespMode.Store(true)
|
||||||
@@ -858,13 +926,22 @@ func chatRound(r *models.ChatRoundReq) error {
|
|||||||
Role: botPersona, Content: "",
|
Role: botPersona, Content: "",
|
||||||
})
|
})
|
||||||
nl := "\n\n"
|
nl := "\n\n"
|
||||||
prevText := textView.GetText(true)
|
prevText := cliPrevOutput
|
||||||
|
if cfg.CLIMode {
|
||||||
if strings.HasSuffix(prevText, nl) {
|
if strings.HasSuffix(prevText, nl) {
|
||||||
nl = ""
|
nl = ""
|
||||||
} else if strings.HasSuffix(prevText, "\n") {
|
} else if strings.HasSuffix(prevText, "\n") {
|
||||||
nl = "\n"
|
nl = "\n"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(textView, "%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
|
} else {
|
||||||
|
prevText = textView.GetText(true)
|
||||||
|
if strings.HasSuffix(prevText, nl) {
|
||||||
|
nl = ""
|
||||||
|
} else if strings.HasSuffix(prevText, "\n") {
|
||||||
|
nl = "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputHandler.Writef("%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
|
||||||
} else {
|
} else {
|
||||||
msgIdx = len(chatBody.Messages) - 1
|
msgIdx = len(chatBody.Messages) - 1
|
||||||
}
|
}
|
||||||
@@ -886,9 +963,9 @@ out:
|
|||||||
thinkingBuffer.WriteString(chunk)
|
thinkingBuffer.WriteString(chunk)
|
||||||
if thinkingCollapsed {
|
if thinkingCollapsed {
|
||||||
// Show placeholder immediately when thinking starts in collapsed mode
|
// Show placeholder immediately when thinking starts in collapsed mode
|
||||||
fmt.Fprint(textView, "[yellow::i][thinking... (press Alt+T to expand)][-:-:-]")
|
outputHandler.Write("[yellow::i][thinking... (press Alt+T to expand)][-:-:-]")
|
||||||
if cfg.AutoScrollEnabled {
|
if cfg.AutoScrollEnabled {
|
||||||
textView.ScrollToEnd()
|
outputHandler.ScrollToEnd()
|
||||||
}
|
}
|
||||||
respText.WriteString(chunk)
|
respText.WriteString(chunk)
|
||||||
continue
|
continue
|
||||||
@@ -903,7 +980,7 @@ out:
|
|||||||
respText.WriteString(chunk)
|
respText.WriteString(chunk)
|
||||||
justExitedThinkingCollapsed = true
|
justExitedThinkingCollapsed = true
|
||||||
if cfg.AutoScrollEnabled {
|
if cfg.AutoScrollEnabled {
|
||||||
textView.ScrollToEnd()
|
outputHandler.ScrollToEnd()
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -920,32 +997,32 @@ out:
|
|||||||
chunk = "\n\n" + chunk
|
chunk = "\n\n" + chunk
|
||||||
justExitedThinkingCollapsed = false
|
justExitedThinkingCollapsed = false
|
||||||
}
|
}
|
||||||
fmt.Fprint(textView, chunk)
|
outputHandler.Write(chunk)
|
||||||
respText.WriteString(chunk)
|
respText.WriteString(chunk)
|
||||||
// Update the message in chatBody.Messages so it persists during Alt+T
|
// Update the message in chatBody.Messages so it persists during Alt+T
|
||||||
if !r.Resume {
|
if !r.Resume {
|
||||||
chatBody.Messages[msgIdx].Content += respText.String()
|
chatBody.Messages[msgIdx].Content += respText.String()
|
||||||
}
|
}
|
||||||
if cfg.AutoScrollEnabled {
|
if cfg.AutoScrollEnabled {
|
||||||
textView.ScrollToEnd()
|
outputHandler.ScrollToEnd()
|
||||||
}
|
}
|
||||||
// Send chunk to audio stream handler
|
// Send chunk to audio stream handler
|
||||||
if cfg.TTS_ENABLED {
|
if cfg.TTS_ENABLED {
|
||||||
TTSTextChan <- chunk
|
TTSTextChan <- chunk
|
||||||
}
|
}
|
||||||
case toolChunk := <-openAIToolChan:
|
case toolChunk := <-openAIToolChan:
|
||||||
fmt.Fprint(textView, toolChunk)
|
outputHandler.Write(toolChunk)
|
||||||
toolResp.WriteString(toolChunk)
|
toolResp.WriteString(toolChunk)
|
||||||
if cfg.AutoScrollEnabled {
|
if cfg.AutoScrollEnabled {
|
||||||
textView.ScrollToEnd()
|
outputHandler.ScrollToEnd()
|
||||||
}
|
}
|
||||||
case <-streamDone:
|
case <-streamDone:
|
||||||
for len(chunkChan) > 0 {
|
for len(chunkChan) > 0 {
|
||||||
chunk := <-chunkChan
|
chunk := <-chunkChan
|
||||||
fmt.Fprint(textView, chunk)
|
outputHandler.Write(chunk)
|
||||||
respText.WriteString(chunk)
|
respText.WriteString(chunk)
|
||||||
if cfg.AutoScrollEnabled {
|
if cfg.AutoScrollEnabled {
|
||||||
textView.ScrollToEnd()
|
outputHandler.ScrollToEnd()
|
||||||
}
|
}
|
||||||
if cfg.TTS_ENABLED {
|
if cfg.TTS_ENABLED {
|
||||||
TTSTextChan <- chunk
|
TTSTextChan <- chunk
|
||||||
@@ -987,8 +1064,7 @@ out:
|
|||||||
cleanChatBody()
|
cleanChatBody()
|
||||||
refreshChatDisplay()
|
refreshChatDisplay()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
// bot msg is done;
|
// bot msg is done; now check it for func call
|
||||||
// now check it for func call
|
|
||||||
// logChat(activeChatName, chatBody.Messages)
|
// logChat(activeChatName, chatBody.Messages)
|
||||||
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
||||||
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
|
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
|
||||||
@@ -999,8 +1075,16 @@ out:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if findCall(respTextNoThink, toolResp.String()) {
|
if findCall(respTextNoThink, toolResp.String()) {
|
||||||
|
// Tool was found and executed, subsequent chatRound will signal cliRespDone when complete
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// No tool call - signal completion now
|
||||||
|
if cfg.CLIMode && cliRespDone != nil {
|
||||||
|
select {
|
||||||
|
case cliRespDone <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
// Check if this message was sent privately to specific characters
|
// Check if this message was sent privately to specific characters
|
||||||
// If so, trigger those characters to respond if that char is not controlled by user
|
// If so, trigger those characters to respond if that char is not controlled by user
|
||||||
// perhaps we should have narrator role to determine which char is next to act
|
// perhaps we should have narrator role to determine which char is next to act
|
||||||
@@ -1229,7 +1313,7 @@ func findCall(msg, toolCall string) bool {
|
|||||||
// return true
|
// return true
|
||||||
// }
|
// }
|
||||||
// Show tool call progress indicator before execution
|
// Show tool call progress indicator before execution
|
||||||
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
outputHandler.Writef("\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
||||||
toolRunningMode.Store(true)
|
toolRunningMode.Store(true)
|
||||||
resp, okT := tools.CallToolWithAgent(fc.Name, fc.Args)
|
resp, okT := tools.CallToolWithAgent(fc.Name, fc.Args)
|
||||||
if !okT {
|
if !okT {
|
||||||
@@ -1307,7 +1391,7 @@ func findCall(msg, toolCall string) bool {
|
|||||||
IsShellCommand: isShellCommand,
|
IsShellCommand: isShellCommand,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
|
outputHandler.Writef("%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
|
||||||
"\n\n", len(chatBody.Messages), cfg.ToolRole, toolResponseMsg.GetText())
|
"\n\n", len(chatBody.Messages), cfg.ToolRole, toolResponseMsg.GetText())
|
||||||
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
||||||
// Clear the stored tool call ID after using it
|
// Clear the stored tool call ID after using it
|
||||||
@@ -1500,6 +1584,31 @@ func refreshLocalModelsIfEmpty() {
|
|||||||
localModelsMu.Unlock()
|
localModelsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startNewCLIChat() []models.RoleMsg {
|
||||||
|
id, err := store.ChatGetMaxID()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to get chat id", "error", err)
|
||||||
|
}
|
||||||
|
id++
|
||||||
|
charToStart(cfg.AssistantRole, false)
|
||||||
|
newChat := &models.Chat{
|
||||||
|
ID: id,
|
||||||
|
Name: fmt.Sprintf("%d_%s", id, cfg.AssistantRole),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Msgs: "",
|
||||||
|
Agent: cfg.AssistantRole,
|
||||||
|
}
|
||||||
|
activeChatName = newChat.Name
|
||||||
|
chatMap[newChat.Name] = newChat
|
||||||
|
cliPrevOutput = ""
|
||||||
|
return chatBody.Messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func startNewCLIErrors() []models.RoleMsg {
|
||||||
|
return startNewCLIChat()
|
||||||
|
}
|
||||||
|
|
||||||
func summarizeAndStartNewChat() {
|
func summarizeAndStartNewChat() {
|
||||||
if len(chatBody.Messages) == 0 {
|
if len(chatBody.Messages) == 0 {
|
||||||
showToast("info", "No chat history to summarize")
|
showToast("info", "No chat history to summarize")
|
||||||
@@ -1526,8 +1635,10 @@ func summarizeAndStartNewChat() {
|
|||||||
}
|
}
|
||||||
chatBody.Messages = append(chatBody.Messages, toolMsg)
|
chatBody.Messages = append(chatBody.Messages, toolMsg)
|
||||||
// Update UI
|
// Update UI
|
||||||
|
if !cfg.CLIMode {
|
||||||
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
|
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
|
||||||
colorText()
|
colorText()
|
||||||
|
}
|
||||||
// Update storage
|
// Update storage
|
||||||
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
||||||
logger.Warn("failed to update storage after injecting summary", "error", err)
|
logger.Warn("failed to update storage after injecting summary", "error", err)
|
||||||
@@ -1585,7 +1696,12 @@ func init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastToolCall = &models.FuncCall{}
|
lastToolCall = &models.FuncCall{}
|
||||||
lastChat := loadOldChatOrGetNew()
|
var lastChat []models.RoleMsg
|
||||||
|
if cfg.CLIMode {
|
||||||
|
lastChat = startNewCLIErrors()
|
||||||
|
} else {
|
||||||
|
lastChat = loadOldChatOrGetNew()
|
||||||
|
}
|
||||||
chatBody = &models.ChatBody{
|
chatBody = &models.ChatBody{
|
||||||
Model: "modelname",
|
Model: "modelname",
|
||||||
Stream: true,
|
Stream: true,
|
||||||
@@ -1620,7 +1736,9 @@ func init() {
|
|||||||
// atomic default values
|
// atomic default values
|
||||||
cachedModelColor.Store("orange")
|
cachedModelColor.Store("orange")
|
||||||
go chatWatcher(ctx)
|
go chatWatcher(ctx)
|
||||||
|
if !cfg.CLIMode {
|
||||||
initTUI()
|
initTUI()
|
||||||
|
}
|
||||||
tools.InitTools(cfg, logger, store)
|
tools.InitTools(cfg, logger, store)
|
||||||
// tooler = tools.InitTools(cfg, logger, store)
|
// tooler = tools.InitTools(cfg, logger, store)
|
||||||
// tooler.RegisterWindowTools(modelHasVision)
|
// tooler.RegisterWindowTools(modelHasVision)
|
||||||
|
|||||||
74
cli-tests/sort-img/check.sh
Executable file
74
cli-tests/sort-img/check.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
LOG_FILE=$(ls -t "$SCRIPT_DIR"/*_run.log 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
log_pass() {
|
||||||
|
echo "[PASS] $1"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
log_fail() {
|
||||||
|
echo "[FAIL] $1"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Checking results ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check has-animals directory exists
|
||||||
|
if [ -d "/tmp/sort-img/has-animals" ]; then
|
||||||
|
log_pass "has-animals directory exists"
|
||||||
|
else
|
||||||
|
log_fail "has-animals directory missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check no-animals directory exists
|
||||||
|
if [ -d "/tmp/sort-img/no-animals" ]; then
|
||||||
|
log_pass "no-animals directory exists"
|
||||||
|
else
|
||||||
|
log_fail "no-animals directory missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check has-animals contains at least one image
|
||||||
|
HAS_ANIMALS_FILES=$(ls -1 /tmp/sort-img/has-animals 2>/dev/null | wc -l)
|
||||||
|
if [ "$HAS_ANIMALS_FILES" -gt 0 ]; then
|
||||||
|
log_pass "has-animals contains images ($HAS_ANIMALS_FILES files)"
|
||||||
|
else
|
||||||
|
log_fail "has-animals is empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check no-animals contains at least one image
|
||||||
|
NO_ANIMALS_FILES=$(ls -1 /tmp/sort-img/no-animals 2>/dev/null | wc -l)
|
||||||
|
if [ "$NO_ANIMALS_FILES" -gt 0 ]; then
|
||||||
|
log_pass "no-animals contains images ($NO_ANIMALS_FILES files)"
|
||||||
|
else
|
||||||
|
log_fail "no-animals is empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check total files sorted correctly (3 original files should be in subdirs)
|
||||||
|
TOTAL_SORTED=$((HAS_ANIMALS_FILES + NO_ANIMALS_FILES))
|
||||||
|
if [ "$TOTAL_SORTED" -eq 3 ]; then
|
||||||
|
log_pass "all 3 files sorted into subdirectories"
|
||||||
|
else
|
||||||
|
log_fail "expected 3 files sorted, got $TOTAL_SORTED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "PASSED: $PASS"
|
||||||
|
echo "FAILED: $FAIL"
|
||||||
|
|
||||||
|
if [ $FAIL -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Log file: $LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All tests passed!"
|
||||||
|
exit 0
|
||||||
25
cli-tests/sort-img/run.sh
Executable file
25
cli-tests/sort-img/run.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
LOG_FILE="$SCRIPT_DIR/${TIMESTAMP}_run.log"
|
||||||
|
|
||||||
|
exec > "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "=== Running teardown ==="
|
||||||
|
"$SCRIPT_DIR/teardown.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Running setup ==="
|
||||||
|
"$SCRIPT_DIR/setup.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Running task ==="
|
||||||
|
TASK=$(cat "$SCRIPT_DIR/task.txt")
|
||||||
|
cd /home/grail/projects/plays/goplays/gf-lt
|
||||||
|
go run . -cli -msg "$TASK"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Done ==="
|
||||||
|
echo "Log file: $LOG_FILE"
|
||||||
9
cli-tests/sort-img/setup.sh
Executable file
9
cli-tests/sort-img/setup.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p /tmp/sort-img
|
||||||
|
|
||||||
|
cp ../../../assets/ex01.png /tmp/sort-img/file1.png
|
||||||
|
cp ../../../assets/helppage.png /tmp/sort-img/file2.png
|
||||||
|
cp ../../../assets/yt_thumb.jpg /tmp/sort-img/file3.jpg
|
||||||
2
cli-tests/sort-img/task.txt
Normal file
2
cli-tests/sort-img/task.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go to /tmp/sort-img, create directories: has-animals, no-animals
|
||||||
|
sort images in /tmp/sort-img into created directories by content
|
||||||
4
cli-tests/sort-img/teardown.sh
Executable file
4
cli-tests/sort-img/teardown.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -rf /tmp/sort-img
|
||||||
91
cli-tests/sort-text/check.sh
Executable file
91
cli-tests/sort-text/check.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
LOG_FILE=$(ls -t "$SCRIPT_DIR"/*_run.log 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
log_pass() {
|
||||||
|
echo "[PASS] $1"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
log_fail() {
|
||||||
|
echo "[FAIL] $1"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Checking results ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check animals directory exists
|
||||||
|
if [ -d "/tmp/sort-text/animals" ]; then
|
||||||
|
log_pass "animals directory exists"
|
||||||
|
else
|
||||||
|
log_fail "animals directory missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check colors directory exists
|
||||||
|
if [ -d "/tmp/sort-text/colors" ]; then
|
||||||
|
log_pass "colors directory exists"
|
||||||
|
else
|
||||||
|
log_fail "colors directory missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check animals contain cat/dog
|
||||||
|
ANIMALS_FILES=$(ls -1 /tmp/sort-text/animals 2>/dev/null | tr '\n' ' ')
|
||||||
|
if echo "$ANIMALS_FILES" | grep -q "file1.txt" && echo "$ANIMALS_FILES" | grep -q "file3.txt"; then
|
||||||
|
log_pass "animals contains animal files"
|
||||||
|
else
|
||||||
|
log_fail "animals missing animal files (got: $ANIMALS_FILES)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check colors contain red/blue
|
||||||
|
COLORS_FILES=$(ls -1 /tmp/sort-text/colors 2>/dev/null | tr '\n' ' ')
|
||||||
|
if echo "$COLORS_FILES" | grep -q "file2.txt" && echo "$COLORS_FILES" | grep -q "file4.txt"; then
|
||||||
|
log_pass "colors contains color files"
|
||||||
|
else
|
||||||
|
log_fail "colors missing color files (got: $COLORS_FILES)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify content
|
||||||
|
if grep -q "cat" /tmp/sort-text/animals/file1.txt 2>/dev/null; then
|
||||||
|
log_pass "file1.txt contains 'cat'"
|
||||||
|
else
|
||||||
|
log_fail "file1.txt missing 'cat'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "dog" /tmp/sort-text/animals/file3.txt 2>/dev/null; then
|
||||||
|
log_pass "file3.txt contains 'dog'"
|
||||||
|
else
|
||||||
|
log_fail "file3.txt missing 'dog'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "red" /tmp/sort-text/colors/file2.txt 2>/dev/null; then
|
||||||
|
log_pass "file2.txt contains 'red'"
|
||||||
|
else
|
||||||
|
log_fail "file2.txt missing 'red'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "blue" /tmp/sort-text/colors/file4.txt 2>/dev/null; then
|
||||||
|
log_pass "file4.txt contains 'blue'"
|
||||||
|
else
|
||||||
|
log_fail "file4.txt missing 'blue'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "PASSED: $PASS"
|
||||||
|
echo "FAILED: $FAIL"
|
||||||
|
|
||||||
|
if [ $FAIL -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Log file: $LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All tests passed!"
|
||||||
|
exit 0
|
||||||
25
cli-tests/sort-text/run.sh
Executable file
25
cli-tests/sort-text/run.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
LOG_FILE="$SCRIPT_DIR/${TIMESTAMP}_run.log"
|
||||||
|
|
||||||
|
exec > "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "=== Running teardown ==="
|
||||||
|
"$SCRIPT_DIR/teardown.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Running setup ==="
|
||||||
|
"$SCRIPT_DIR/setup.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Running task ==="
|
||||||
|
TASK=$(cat "$SCRIPT_DIR/task.txt")
|
||||||
|
cd /home/grail/projects/plays/goplays/gf-lt
|
||||||
|
go run . -cli -msg "$TASK"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Done ==="
|
||||||
|
echo "Log file: $LOG_FILE"
|
||||||
10
cli-tests/sort-text/setup.sh
Executable file
10
cli-tests/sort-text/setup.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p /tmp/sort-text
|
||||||
|
|
||||||
|
printf "cat" > /tmp/sort-text/file1.txt
|
||||||
|
printf "red" > /tmp/sort-text/file2.txt
|
||||||
|
printf "dog" > /tmp/sort-text/file3.txt
|
||||||
|
printf "blue" > /tmp/sort-text/file4.txt
|
||||||
2
cli-tests/sort-text/task.txt
Normal file
2
cli-tests/sort-text/task.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
go to /tmp/sort-text, create directories: animals, colors
|
||||||
|
sort /tmp/sort-text/*.txt into created directories by text content
|
||||||
4
cli-tests/sort-text/teardown.sh
Executable file
4
cli-tests/sort-text/teardown.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -rf /tmp/sort-text
|
||||||
@@ -18,7 +18,7 @@ EmbedTokenizerPath = "onnx/embedgemma/tokenizer.json"
|
|||||||
EmbedDims = 768
|
EmbedDims = 768
|
||||||
#
|
#
|
||||||
ShowSys = true
|
ShowSys = true
|
||||||
LogFile = "log.txt"
|
LogFile = "log.log"
|
||||||
UserRole = "user"
|
UserRole = "user"
|
||||||
ToolRole = "tool"
|
ToolRole = "tool"
|
||||||
AssistantRole = "assistant"
|
AssistantRole = "assistant"
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ type Config struct {
|
|||||||
// playwright browser
|
// playwright browser
|
||||||
PlaywrightEnabled bool `toml:"PlaywrightEnabled"`
|
PlaywrightEnabled bool `toml:"PlaywrightEnabled"`
|
||||||
PlaywrightDebug bool `toml:"PlaywrightDebug"` // !headless
|
PlaywrightDebug bool `toml:"PlaywrightDebug"` // !headless
|
||||||
|
// CLI mode
|
||||||
|
CLIMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(fn string) (*Config, error) {
|
func LoadConfig(fn string) (*Config, error) {
|
||||||
|
|||||||
255
main.go
255
main.go
@@ -1,6 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"gf-lt/models"
|
||||||
|
"gf-lt/pngmeta"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
@@ -22,9 +31,22 @@ var (
|
|||||||
statusLineTempl = "help (F12) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10) | API: [orange:-:b]%s[-:-:-] (ctrl+v)\nwriting as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
|
statusLineTempl = "help (F12) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10) | API: [orange:-:b]%s[-:-:-] (ctrl+v)\nwriting as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
|
||||||
focusSwitcher = map[tview.Primitive]tview.Primitive{}
|
focusSwitcher = map[tview.Primitive]tview.Primitive{}
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
|
cliCardPath string
|
||||||
|
cliContinue bool
|
||||||
|
cliMsg string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.BoolVar(&cfg.CLIMode, "cli", false, "Run in CLI mode without TUI")
|
||||||
|
flag.BoolVar(&cfg.ToolUse, "tools", true, "run with tools")
|
||||||
|
flag.StringVar(&cliCardPath, "card", "", "Path to syscard JSON file")
|
||||||
|
flag.BoolVar(&cliContinue, "continue", false, "Continue from last chat (by agent or card)")
|
||||||
|
flag.StringVar(&cliMsg, "msg", "", "Send message and exit (one-shot mode)")
|
||||||
|
flag.Parse()
|
||||||
|
if cfg.CLIMode {
|
||||||
|
runCLIMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
pages.AddPage("main", flex, true, true)
|
pages.AddPage("main", flex, true, true)
|
||||||
if err := app.SetRoot(pages,
|
if err := app.SetRoot(pages,
|
||||||
true).EnableMouse(cfg.EnableMouse).EnablePaste(true).Run(); err != nil {
|
true).EnableMouse(cfg.EnableMouse).EnablePaste(true).Run(); err != nil {
|
||||||
@@ -32,3 +54,236 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runCLIMode() {
|
||||||
|
outputHandler = &CLIOutputHandler{}
|
||||||
|
cliRespDone = make(chan bool, 1)
|
||||||
|
if cliCardPath != "" {
|
||||||
|
card, err := pngmeta.ReadCardJson(cliCardPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to load syscard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cfg.AssistantRole = card.Role
|
||||||
|
sysMap[card.ID] = card
|
||||||
|
roleToID[card.Role] = card.ID
|
||||||
|
charToStart(card.Role, false)
|
||||||
|
fmt.Printf("Loaded syscard: %s (%s)\n", card.Role, card.FilePath)
|
||||||
|
}
|
||||||
|
if cliContinue {
|
||||||
|
if cliCardPath != "" {
|
||||||
|
history, err := loadAgentsLastChat(cfg.AssistantRole)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("No previous chat found for %s, starting new chat\n", cfg.AssistantRole)
|
||||||
|
startNewCLIChat()
|
||||||
|
} else {
|
||||||
|
chatBody.Messages = history
|
||||||
|
fmt.Printf("Continued chat: %s\n", activeChatName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chatBody.Messages = loadOldChatOrGetNew()
|
||||||
|
fmt.Printf("Continued chat: %s\n", activeChatName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startNewCLIChat()
|
||||||
|
}
|
||||||
|
printCLIWelcome()
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
if cliMsg != "" {
|
||||||
|
persona := cfg.UserRole
|
||||||
|
if cfg.WriteNextMsgAs != "" {
|
||||||
|
persona = cfg.WriteNextMsgAs
|
||||||
|
}
|
||||||
|
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: cliMsg}
|
||||||
|
<-cliRespDone
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Print("> ")
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
msg := scanner.Text()
|
||||||
|
if msg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(msg, "/") {
|
||||||
|
if !handleCLICommand(msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
persona := cfg.UserRole
|
||||||
|
if cfg.WriteNextMsgAs != "" {
|
||||||
|
persona = cfg.WriteNextMsgAs
|
||||||
|
}
|
||||||
|
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msg}
|
||||||
|
<-cliRespDone
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCLIWelcome() {
|
||||||
|
fmt.Println("CLI Mode started. Type your messages or commands.")
|
||||||
|
fmt.Println("Type /help for available commands.")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCLIHelp() {
|
||||||
|
fmt.Println("Available commands:")
|
||||||
|
fmt.Println(" /help, /h - Show this help message")
|
||||||
|
fmt.Println(" /new, /n - Start a new chat (clears conversation)")
|
||||||
|
fmt.Println(" /card <path>, /c <path> - Load a different syscard")
|
||||||
|
fmt.Println(" /undo, /u - Delete last message")
|
||||||
|
fmt.Println(" /history, /ls - List chat history")
|
||||||
|
fmt.Println(" /load <name> - Load a specific chat by name")
|
||||||
|
fmt.Println(" /model <name>, /m <name> - Switch model")
|
||||||
|
fmt.Println(" /api <index>, /a <index> - Switch API link (no index to list)")
|
||||||
|
fmt.Println(" /quit, /q, /exit - Exit CLI mode")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Current syscard: %s\n", cfg.AssistantRole)
|
||||||
|
fmt.Printf("Current model: %s\n", chatBody.Model)
|
||||||
|
fmt.Printf("Current API: %s\n", cfg.CurrentAPI)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCLICommand(msg string) bool {
|
||||||
|
parts := strings.Fields(msg)
|
||||||
|
cmd := strings.ToLower(parts[0])
|
||||||
|
args := parts[1:]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "/help", "/h":
|
||||||
|
printCLIHelp()
|
||||||
|
case "/new", "/n":
|
||||||
|
startNewCLIChat()
|
||||||
|
fmt.Println("New chat started.")
|
||||||
|
fmt.Printf("Syscard: %s\n", cfg.AssistantRole)
|
||||||
|
fmt.Println()
|
||||||
|
case "/card", "/c":
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("Usage: /card <path>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
card, err := pngmeta.ReadCardJson(args[0])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to load syscard: %v\n", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
cfg.AssistantRole = card.Role
|
||||||
|
sysMap[card.ID] = card
|
||||||
|
roleToID[card.Role] = card.ID
|
||||||
|
charToStart(card.Role, false)
|
||||||
|
startNewCLIChat()
|
||||||
|
fmt.Printf("Switched to syscard: %s (%s)\n", card.Role, card.FilePath)
|
||||||
|
case "/undo", "/u":
|
||||||
|
if len(chatBody.Messages) == 0 {
|
||||||
|
fmt.Println("No messages to delete.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
|
||||||
|
cliPrevOutput = ""
|
||||||
|
fmt.Println("Last message deleted.")
|
||||||
|
case "/history", "/ls":
|
||||||
|
fmt.Println("Chat history:")
|
||||||
|
for name := range chatMap {
|
||||||
|
marker := " "
|
||||||
|
if name == activeChatName {
|
||||||
|
marker = "* "
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s\n", marker, name)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
case "/load":
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("Usage: /load <name>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
chat, ok := chatMap[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("Chat not found: %s\n", name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
history, err := chat.ToHistory()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to load chat: %v\n", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
chatBody.Messages = history
|
||||||
|
activeChatName = name
|
||||||
|
cfg.AssistantRole = chat.Agent
|
||||||
|
fmt.Printf("Loaded chat: %s\n", name)
|
||||||
|
case "/model", "/m":
|
||||||
|
getModelListForAPI := func(api string) []string {
|
||||||
|
if strings.Contains(api, "api.deepseek.com/") {
|
||||||
|
return []string{"deepseek-chat", "deepseek-reasoner"}
|
||||||
|
} else if strings.Contains(api, "openrouter.ai") {
|
||||||
|
return ORFreeModels
|
||||||
|
}
|
||||||
|
return LocalModels
|
||||||
|
}
|
||||||
|
modelList := getModelListForAPI(cfg.CurrentAPI)
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("Models:")
|
||||||
|
for i, model := range modelList {
|
||||||
|
marker := " "
|
||||||
|
if model == chatBody.Model {
|
||||||
|
marker = "* "
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%d: %s\n", marker, i, model)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nCurrent model: %s\n", chatBody.Model)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Try index first, then model name
|
||||||
|
if idx, err := strconv.Atoi(args[0]); err == nil && idx >= 0 && idx < len(modelList) {
|
||||||
|
chatBody.Model = modelList[idx]
|
||||||
|
fmt.Printf("Switched to model: %s\n", chatBody.Model)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if slices.Index(modelList, args[0]) < 0 {
|
||||||
|
fmt.Printf("Model '%s' not found. Use index or choose from:\n", args[0])
|
||||||
|
for i, model := range modelList {
|
||||||
|
fmt.Printf(" %d: %s\n", i, model)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
chatBody.Model = args[0]
|
||||||
|
fmt.Printf("Switched to model: %s\n", args[0])
|
||||||
|
case "/api", "/a":
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("API Links:")
|
||||||
|
for i, link := range cfg.ApiLinks {
|
||||||
|
marker := " "
|
||||||
|
if link == cfg.CurrentAPI {
|
||||||
|
marker = "* "
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%d: %s\n", marker, i, link)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nCurrent API: %s\n", cfg.CurrentAPI)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
idx := 0
|
||||||
|
fmt.Sscanf(args[0], "%d", &idx)
|
||||||
|
if idx < 0 || idx >= len(cfg.ApiLinks) {
|
||||||
|
fmt.Printf("Invalid index. Valid range: 0-%d\n", len(cfg.ApiLinks)-1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
cfg.CurrentAPI = cfg.ApiLinks[idx]
|
||||||
|
fmt.Printf("Switched to API: %s\n", cfg.CurrentAPI)
|
||||||
|
case "/quit", "/q", "/exit":
|
||||||
|
fmt.Println("Goodbye!")
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unknown command: %s\n", msg)
|
||||||
|
fmt.Println("Type /help for available commands.")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
131
tools/chain.go
131
tools/chain.go
@@ -150,7 +150,7 @@ func execSingle(command, stdin string) (string, error) {
|
|||||||
name := parts[0]
|
name := parts[0]
|
||||||
args := parts[1:]
|
args := parts[1:]
|
||||||
// Check if it's a built-in Go command
|
// Check if it's a built-in Go command
|
||||||
if result := execBuiltin(name, args, stdin); result != "" {
|
if result, isBuiltin := execBuiltin(name, args, stdin); isBuiltin {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
// Otherwise execute as system command
|
// Otherwise execute as system command
|
||||||
@@ -201,21 +201,23 @@ func tokenize(input string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// execBuiltin executes a built-in command if it exists.
|
// execBuiltin executes a built-in command if it exists.
|
||||||
func execBuiltin(name string, args []string, stdin string) string {
|
// Returns (result, true) if it was a built-in (even if result is empty).
|
||||||
|
// Returns ("", false) if it's not a built-in command.
|
||||||
|
func execBuiltin(name string, args []string, stdin string) (string, bool) {
|
||||||
switch name {
|
switch name {
|
||||||
case "echo":
|
case "echo":
|
||||||
if stdin != "" {
|
if stdin != "" {
|
||||||
return stdin
|
return stdin, true
|
||||||
}
|
}
|
||||||
return strings.Join(args, " ")
|
return strings.Join(args, " "), true
|
||||||
case "time":
|
case "time":
|
||||||
return "2006-01-02 15:04:05 MST"
|
return "2006-01-02 15:04:05 MST", true
|
||||||
case "cat":
|
case "cat":
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if stdin != "" {
|
if stdin != "" {
|
||||||
return stdin
|
return stdin, true
|
||||||
}
|
}
|
||||||
return ""
|
return "", true
|
||||||
}
|
}
|
||||||
path := args[0]
|
path := args[0]
|
||||||
abs := path
|
abs := path
|
||||||
@@ -224,14 +226,14 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
}
|
}
|
||||||
data, err := os.ReadFile(abs)
|
data, err := os.ReadFile(abs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] cat: %v", err)
|
return fmt.Sprintf("[error] cat: %v", err), true
|
||||||
}
|
}
|
||||||
return string(data)
|
return string(data), true
|
||||||
case "pwd":
|
case "pwd":
|
||||||
return cfg.FilePickerDir
|
return cfg.FilePickerDir, true
|
||||||
case "cd":
|
case "cd":
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "[error] usage: cd <dir>"
|
return "[error] usage: cd <dir>", true
|
||||||
}
|
}
|
||||||
dir := args[0]
|
dir := args[0]
|
||||||
// Resolve relative to cfg.FilePickerDir
|
// Resolve relative to cfg.FilePickerDir
|
||||||
@@ -242,16 +244,16 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
abs = filepath.Clean(abs)
|
abs = filepath.Clean(abs)
|
||||||
info, err := os.Stat(abs)
|
info, err := os.Stat(abs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] cd: %v", err)
|
return fmt.Sprintf("[error] cd: %v", err), true
|
||||||
}
|
}
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
return "[error] cd: not a directory: " + dir
|
return "[error] cd: not a directory: " + dir, true
|
||||||
}
|
}
|
||||||
cfg.FilePickerDir = abs
|
cfg.FilePickerDir = abs
|
||||||
return "Changed directory to: " + cfg.FilePickerDir
|
return "Changed directory to: " + cfg.FilePickerDir, true
|
||||||
case "mkdir":
|
case "mkdir":
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "[error] usage: mkdir [-p] <dir>"
|
return "[error] usage: mkdir [-p] <dir>", true
|
||||||
}
|
}
|
||||||
createParents := false
|
createParents := false
|
||||||
var dirPath string
|
var dirPath string
|
||||||
@@ -263,7 +265,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dirPath == "" {
|
if dirPath == "" {
|
||||||
return "[error] usage: mkdir [-p] <dir>"
|
return "[error] usage: mkdir [-p] <dir>", true
|
||||||
}
|
}
|
||||||
abs := dirPath
|
abs := dirPath
|
||||||
if !filepath.IsAbs(dirPath) {
|
if !filepath.IsAbs(dirPath) {
|
||||||
@@ -277,12 +279,12 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
mkdirFunc = os.Mkdir
|
mkdirFunc = os.Mkdir
|
||||||
}
|
}
|
||||||
if err := mkdirFunc(abs, 0o755); err != nil {
|
if err := mkdirFunc(abs, 0o755); err != nil {
|
||||||
return fmt.Sprintf("[error] mkdir: %v", err)
|
return fmt.Sprintf("[error] mkdir: %v", err), true
|
||||||
}
|
}
|
||||||
if createParents {
|
if createParents {
|
||||||
return "Created " + dirPath + " (with parents)"
|
return "Created " + dirPath + " (with parents)", true
|
||||||
}
|
}
|
||||||
return "Created " + dirPath
|
return "Created " + dirPath, true
|
||||||
case "ls":
|
case "ls":
|
||||||
dir := "."
|
dir := "."
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
@@ -297,7 +299,7 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
}
|
}
|
||||||
entries, err := os.ReadDir(abs)
|
entries, err := os.ReadDir(abs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] ls: %v", err)
|
return fmt.Sprintf("[error] ls: %v", err), true
|
||||||
}
|
}
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -317,21 +319,98 @@ func execBuiltin(name string, args []string, stdin string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if out.Len() == 0 {
|
if out.Len() == 0 {
|
||||||
return "(empty directory)"
|
return "(empty directory)", true
|
||||||
}
|
}
|
||||||
return strings.TrimRight(out.String(), "\n")
|
return strings.TrimRight(out.String(), "\n"), true
|
||||||
case "go":
|
case "go":
|
||||||
// Allow all go subcommands
|
// Allow all go subcommands
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "[error] usage: go <subcommand> [options]"
|
return "[error] usage: go <subcommand> [options]", true
|
||||||
}
|
}
|
||||||
cmd := exec.Command("go", args...)
|
cmd := exec.Command("go", args...)
|
||||||
cmd.Dir = cfg.FilePickerDir
|
cmd.Dir = cfg.FilePickerDir
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] go %s: %v\n%s", args[0], err, string(output))
|
return fmt.Sprintf("[error] go %s: %v\n%s", args[0], err, string(output)), true
|
||||||
}
|
}
|
||||||
return string(output)
|
return string(output), true
|
||||||
|
case "cp":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "[error] usage: cp <source> <dest>", true
|
||||||
}
|
}
|
||||||
return ""
|
src := args[0]
|
||||||
|
dst := args[1]
|
||||||
|
if !filepath.IsAbs(src) {
|
||||||
|
src = filepath.Join(cfg.FilePickerDir, src)
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(dst) {
|
||||||
|
dst = filepath.Join(cfg.FilePickerDir, dst)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp: %v", err), true
|
||||||
|
}
|
||||||
|
err = os.WriteFile(dst, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp: %v", err), true
|
||||||
|
}
|
||||||
|
return "Copied " + src + " to " + dst, true
|
||||||
|
case "mv":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "[error] usage: mv <source> <dest>", true
|
||||||
|
}
|
||||||
|
src := args[0]
|
||||||
|
dst := args[1]
|
||||||
|
if !filepath.IsAbs(src) {
|
||||||
|
src = filepath.Join(cfg.FilePickerDir, src)
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(dst) {
|
||||||
|
dst = filepath.Join(cfg.FilePickerDir, dst)
|
||||||
|
}
|
||||||
|
err := os.Rename(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv: %v", err), true
|
||||||
|
}
|
||||||
|
return "Moved " + src + " to " + dst, true
|
||||||
|
case "rm":
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "[error] usage: rm [-r] <file>", true
|
||||||
|
}
|
||||||
|
recursive := false
|
||||||
|
var target string
|
||||||
|
for _, a := range args {
|
||||||
|
if a == "-r" || a == "-rf" || a == "-fr" || a == "-recursive" {
|
||||||
|
recursive = true
|
||||||
|
} else if target == "" {
|
||||||
|
target = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target == "" {
|
||||||
|
return "[error] usage: rm [-r] <file>", true
|
||||||
|
}
|
||||||
|
abs := target
|
||||||
|
if !filepath.IsAbs(target) {
|
||||||
|
abs = filepath.Join(cfg.FilePickerDir, target)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(abs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] rm: %v", err), true
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
if recursive {
|
||||||
|
err = os.RemoveAll(abs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] rm: %v", err), true
|
||||||
|
}
|
||||||
|
return "Removed " + abs, true
|
||||||
|
}
|
||||||
|
return "[error] rm: is a directory (use -r)", true
|
||||||
|
}
|
||||||
|
err = os.Remove(abs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] rm: %v", err), true
|
||||||
|
}
|
||||||
|
return "Removed " + abs, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,10 +129,10 @@ func (t *Tools) initAgentsB() {
|
|||||||
agent.RegisterB("summarize_chat", agent.NewWebAgentB(t.webAgentClient, summarySysPrompt))
|
agent.RegisterB("summarize_chat", agent.NewWebAgentB(t.webAgentClient, summarySysPrompt))
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitTools(cfg *config.Config, logger *slog.Logger, store storage.FullRepo) *Tools {
|
func InitTools(initCfg *config.Config, logger *slog.Logger, store storage.FullRepo) *Tools {
|
||||||
_ = logger
|
logger = logger
|
||||||
_ = cfg
|
cfg = initCfg
|
||||||
if cfg.PlaywrightEnabled {
|
if initCfg.PlaywrightEnabled {
|
||||||
if err := CheckPlaywright(); err != nil {
|
if err := CheckPlaywright(); err != nil {
|
||||||
// slow, need a faster check if playwright install
|
// slow, need a faster check if playwright install
|
||||||
if err := InstallPW(); err != nil {
|
if err := InstallPW(); err != nil {
|
||||||
@@ -686,7 +686,7 @@ Use: run "command" to execute.`
|
|||||||
-c count matches
|
-c count matches
|
||||||
Example:
|
Example:
|
||||||
run "grep error" (from stdin)
|
run "grep error" (from stdin)
|
||||||
run "grep -i warning log.txt"`
|
run "grep -i warn log.txt"`
|
||||||
case "cd":
|
case "cd":
|
||||||
return `cd <directory>
|
return `cd <directory>
|
||||||
Change working directory.
|
Change working directory.
|
||||||
|
|||||||
1
tui.go
1
tui.go
@@ -230,6 +230,7 @@ func initTUI() {
|
|||||||
tview.Styles = colorschemes["default"]
|
tview.Styles = colorschemes["default"]
|
||||||
app = tview.NewApplication()
|
app = tview.NewApplication()
|
||||||
pages = tview.NewPages()
|
pages = tview.NewPages()
|
||||||
|
outputHandler = &TUIOutputHandler{tv: textView}
|
||||||
shellInput = tview.NewInputField().
|
shellInput = tview.NewInputField().
|
||||||
SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)). // dynamic prompt
|
SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)). // dynamic prompt
|
||||||
SetFieldWidth(0).
|
SetFieldWidth(0).
|
||||||
|
|||||||
Reference in New Issue
Block a user