Compare commits
1 Commits
master
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2687f38d00 |
@@ -38,8 +38,3 @@ func RegisterA(toolNames []string, a AgenterA) {
|
|||||||
func Get(toolName string) AgenterB {
|
func Get(toolName string) AgenterB {
|
||||||
return RegistryB[toolName]
|
return RegistryB[toolName]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register is a convenience wrapper for RegisterB.
|
|
||||||
func Register(toolName string, a AgenterB) {
|
|
||||||
RegisterB(toolName, a)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ func detectAPI(api string) (isCompletion, isChat, isDeepSeek, isOpenRouter bool)
|
|||||||
type AgentClient struct {
|
type AgentClient struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
getToken func() string
|
getToken func() string
|
||||||
log slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentClient(cfg *config.Config, log slog.Logger, gt func() string) *AgentClient {
|
func NewAgentClient(cfg *config.Config, log *slog.Logger, gt func() string) *AgentClient {
|
||||||
return &AgentClient{
|
return &AgentClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
getToken: gt,
|
getToken: gt,
|
||||||
@@ -44,7 +44,7 @@ func NewAgentClient(cfg *config.Config, log slog.Logger, gt func() string) *Agen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ag *AgentClient) Log() *slog.Logger {
|
func (ag *AgentClient) Log() *slog.Logger {
|
||||||
return &ag.log
|
return ag.log
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ag *AgentClient) FormMsg(sysprompt, msg string) (io.Reader, error) {
|
func (ag *AgentClient) FormMsg(sysprompt, msg string) (io.Reader, error) {
|
||||||
@@ -63,11 +63,9 @@ func (ag *AgentClient) buildRequest(sysprompt, msg string) ([]byte, error) {
|
|||||||
{Role: "system", Content: sysprompt},
|
{Role: "system", Content: sysprompt},
|
||||||
{Role: "user", Content: msg},
|
{Role: "user", Content: msg},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine API type
|
// Determine API type
|
||||||
isCompletion, isChat, isDeepSeek, isOpenRouter := detectAPI(api)
|
isCompletion, isChat, isDeepSeek, isOpenRouter := detectAPI(api)
|
||||||
ag.log.Debug("agent building request", "api", api, "isCompletion", isCompletion, "isChat", isChat, "isDeepSeek", isDeepSeek, "isOpenRouter", isOpenRouter)
|
ag.log.Debug("agent building request", "api", api, "isCompletion", isCompletion, "isChat", isChat, "isDeepSeek", isDeepSeek, "isOpenRouter", isOpenRouter)
|
||||||
|
|
||||||
// Build prompt for completion endpoints
|
// Build prompt for completion endpoints
|
||||||
if isCompletion {
|
if isCompletion {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -76,7 +74,6 @@ func (ag *AgentClient) buildRequest(sysprompt, msg string) ([]byte, error) {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
prompt := strings.TrimSpace(sb.String())
|
prompt := strings.TrimSpace(sb.String())
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isDeepSeek:
|
case isDeepSeek:
|
||||||
// DeepSeek completion
|
// DeepSeek completion
|
||||||
@@ -95,7 +92,6 @@ func (ag *AgentClient) buildRequest(sysprompt, msg string) ([]byte, error) {
|
|||||||
return json.Marshal(req)
|
return json.Marshal(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat completions endpoints
|
// Chat completions endpoints
|
||||||
if isChat || !isCompletion {
|
if isChat || !isCompletion {
|
||||||
chatBody := &models.ChatBody{
|
chatBody := &models.ChatBody{
|
||||||
@@ -103,7 +99,6 @@ func (ag *AgentClient) buildRequest(sysprompt, msg string) ([]byte, error) {
|
|||||||
Stream: false, // Agents don't need streaming
|
Stream: false, // Agents don't need streaming
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isDeepSeek:
|
case isDeepSeek:
|
||||||
// DeepSeek chat
|
// DeepSeek chat
|
||||||
@@ -122,7 +117,6 @@ func (ag *AgentClient) buildRequest(sysprompt, msg string) ([]byte, error) {
|
|||||||
return json.Marshal(req)
|
return json.Marshal(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback (should not reach here)
|
// Fallback (should not reach here)
|
||||||
ag.log.Warn("unknown API, using default chat completions format", "api", api)
|
ag.log.Warn("unknown API, using default chat completions format", "api", api)
|
||||||
chatBody := &models.ChatBody{
|
chatBody := &models.ChatBody{
|
||||||
@@ -165,7 +159,6 @@ func (ag *AgentClient) LLMRequest(body io.Reader) ([]byte, error) {
|
|||||||
ag.log.Error("agent LLM request failed", "status", resp.StatusCode, "response", string(responseBytes[:min(len(responseBytes), 1000)]))
|
ag.log.Error("agent LLM request failed", "status", resp.StatusCode, "response", string(responseBytes[:min(len(responseBytes), 1000)]))
|
||||||
return responseBytes, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(responseBytes[:min(len(responseBytes), 200)]))
|
return responseBytes, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(responseBytes[:min(len(responseBytes), 200)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response and extract text content
|
// Parse response and extract text content
|
||||||
text, err := extractTextFromResponse(responseBytes)
|
text, err := extractTextFromResponse(responseBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,17 +172,16 @@ func (ag *AgentClient) LLMRequest(body io.Reader) ([]byte, error) {
|
|||||||
// extractTextFromResponse parses common LLM response formats and extracts the text content.
|
// extractTextFromResponse parses common LLM response formats and extracts the text content.
|
||||||
func extractTextFromResponse(data []byte) (string, error) {
|
func extractTextFromResponse(data []byte) (string, error) {
|
||||||
// Try to parse as generic JSON first
|
// Try to parse as generic JSON first
|
||||||
var genericResp map[string]interface{}
|
var genericResp map[string]any
|
||||||
if err := json.Unmarshal(data, &genericResp); err != nil {
|
if err := json.Unmarshal(data, &genericResp); err != nil {
|
||||||
// Not JSON, return as string
|
// Not JSON, return as string
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for OpenAI chat completion format
|
// Check for OpenAI chat completion format
|
||||||
if choices, ok := genericResp["choices"].([]interface{}); ok && len(choices) > 0 {
|
if choices, ok := genericResp["choices"].([]any); ok && len(choices) > 0 {
|
||||||
if firstChoice, ok := choices[0].(map[string]interface{}); ok {
|
if firstChoice, ok := choices[0].(map[string]any); ok {
|
||||||
// Chat completion: choices[0].message.content
|
// Chat completion: choices[0].message.content
|
||||||
if message, ok := firstChoice["message"].(map[string]interface{}); ok {
|
if message, ok := firstChoice["message"].(map[string]any); ok {
|
||||||
if content, ok := message["content"].(string); ok {
|
if content, ok := message["content"].(string); ok {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
@@ -199,19 +191,17 @@ func extractTextFromResponse(data []byte) (string, error) {
|
|||||||
return text, nil
|
return text, nil
|
||||||
}
|
}
|
||||||
// Delta format for streaming (should not happen with stream: false)
|
// Delta format for streaming (should not happen with stream: false)
|
||||||
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
|
if delta, ok := firstChoice["delta"].(map[string]any); ok {
|
||||||
if content, ok := delta["content"].(string); ok {
|
if content, ok := delta["content"].(string); ok {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for llama.cpp completion format
|
// Check for llama.cpp completion format
|
||||||
if content, ok := genericResp["content"].(string); ok {
|
if content, ok := genericResp["content"].(string); ok {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown format, return pretty-printed JSON
|
// Unknown format, return pretty-printed JSON
|
||||||
prettyJSON, err := json.MarshalIndent(genericResp, "", " ")
|
prettyJSON, err := json.MarshalIndent(genericResp, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -219,10 +209,3 @@ func extractTextFromResponse(data []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(prettyJSON), nil
|
return string(prettyJSON), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ func updateFlexLayout() {
|
|||||||
if shellMode {
|
if shellMode {
|
||||||
flex.AddItem(shellInput, 0, 10, false)
|
flex.AddItem(shellInput, 0, 10, false)
|
||||||
} else {
|
} else {
|
||||||
flex.AddItem(bottomFlex, 0, 10, true)
|
flex.AddItem(textArea, 0, 10, false)
|
||||||
}
|
}
|
||||||
if positionVisible {
|
if positionVisible {
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
flex.AddItem(statusLineWidget, 0, 2, false)
|
||||||
|
|||||||
7
sysprompts/cluedo.json
Normal file
7
sysprompts/cluedo.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sys_prompt": "A game of cluedo. Players are {{user}}, {{char}}, {{char2}};\n\nrooms: hall, lounge, dinning room kitchen, ballroom, conservatory, billiard room, library, study;\nweapons: candlestick, dagger, lead pipe, revolver, rope, spanner;\npeople: miss Scarlett, colonel Mustard, mrs. White, reverend Green, mrs. Peacock, professor Plum;\n\nA murder happened in a mansion with 9 rooms. Victim is dr. Black.\nPlayers goal is to find out who commited a murder, in what room and with what weapon.\nWeapons, people and rooms not involved in murder are distributed between players (as cards) by tool agent.\nThe objective of the game is to deduce the details of the murder. There are six characters, six murder weapons, and nine rooms, leaving the players with 324 possibilities. As soon as a player enters a room, they may make a suggestion as to the details, naming a suspect, the room they are in, and the weapon. For example: \"I suspect Professor Plum, in the Dining Room, with the candlestick\".\nOnce a player makes a suggestion, the others are called upon to disprove it.\nBefore the player's move, tool agent will remind that players their cards. There are two types of moves: making a suggestion (suggestion_move) and disproving other player suggestion (evidence_move);\nIn this version player wins when the correct details are named in the suggestion_move.\n\n<example_game>\n{{user}}:\nlet's start a game of cluedo!\ntool: cards of {{char}} are 'LEAD PIPE', 'BALLROOM', 'CONSERVATORY', 'STUDY', 'Mrs. White'; suggestion_move;\n{{char}}:\n(putting miss Scarlet into the Hall with the Revolver) \"I suspect miss Scarlett, in the Hall, with the revolver.\"\ntool: cards of {{char2}} are 'SPANNER', 'DAGGER', 'Professor Plum', 'LIBRARY', 'Mrs. Peacock'; evidence_move;\n{{char2}}:\n\"No objections.\" (no cards matching the suspicion of {{char}})\ntool: cards of {{user}} are 'Colonel Mustard', 'Miss Scarlett', 'DINNING ROOM', 'CANDLESTICK', 'HALL'; evidence_move;\n{{user}}:\n\"I object. Miss Scarlett is innocent.\" (shows card with 'Miss Scarlett')\ntool: cards of {{char2}} are 'SPANNER', 'DAGGER', 'Professor Plum', 'LIBRARY', 'Mrs. Peacock'; suggestion_move;\n{{char2}}:\n*So it was not Miss Scarlett, good to know.*\n(moves Mrs. White to the Billiard Room) \"It might have been Mrs. White, in the Billiard Room, with the Revolver.\"\ntool: cards of {{user}} are 'Colonel Mustard', 'Miss Scarlett', 'DINNING ROOM', 'CANDLESTICK', 'HALL'; evidence_move;\n{{user}}:\n(no matching cards for the assumption of {{char2}}) \"Sounds possible to me.\"\ntool: cards of {{char}} are 'LEAD PIPE', 'BALLROOM', 'CONSERVATORY', 'STUDY', 'Mrs. White'; evidence_move;\n{{char}}:\n(shows Mrs. White card) \"No. Was not Mrs. White\"\ntool: cards of {{user}} are 'Colonel Mustard', 'Miss Scarlett', 'DINNING ROOM', 'CANDLESTICK', 'HALL'; suggestion_move;\n{{user}}:\n*So not Mrs. White...* (moves Reverend Green into the Billiard Room) \"I suspect Reverend Green, in the Billiard Room, with the Revolver.\"\ntool: Correct. It was Reverend Green in the Billiard Room, with the revolver. {{user}} wins.\n</example_game>",
|
||||||
|
"role": "CluedoPlayer",
|
||||||
|
"role2": "CluedoEnjoyer",
|
||||||
|
"filepath": "sysprompts/cluedo.json",
|
||||||
|
"first_msg": "Hey guys! Want to play cluedo?"
|
||||||
|
}
|
||||||
22
tools.go
22
tools.go
@@ -278,25 +278,13 @@ func updateToolCapabilities() {
|
|||||||
// getWebAgentClient returns a singleton AgentClient for web agents.
|
// getWebAgentClient returns a singleton AgentClient for web agents.
|
||||||
func getWebAgentClient() *agent.AgentClient {
|
func getWebAgentClient() *agent.AgentClient {
|
||||||
webAgentClientOnce.Do(func() {
|
webAgentClientOnce.Do(func() {
|
||||||
if cfg == nil {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("web agent client unavailable: config not initialized")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if logger == nil {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("web agent client unavailable: logger not initialized")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getToken := func() string {
|
getToken := func() string {
|
||||||
if chunkParser == nil {
|
if chunkParser == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return chunkParser.GetToken()
|
return chunkParser.GetToken()
|
||||||
}
|
}
|
||||||
webAgentClient = agent.NewAgentClient(cfg, *logger, getToken)
|
webAgentClient = agent.NewAgentClient(cfg, logger, getToken)
|
||||||
})
|
})
|
||||||
return webAgentClient
|
return webAgentClient
|
||||||
}
|
}
|
||||||
@@ -306,13 +294,13 @@ func registerWebAgents() {
|
|||||||
webAgentsOnce.Do(func() {
|
webAgentsOnce.Do(func() {
|
||||||
client := getWebAgentClient()
|
client := getWebAgentClient()
|
||||||
// Register rag_search agent
|
// Register rag_search agent
|
||||||
agent.Register("rag_search", agent.NewWebAgentB(client, ragSearchSysPrompt))
|
agent.RegisterB("rag_search", agent.NewWebAgentB(client, ragSearchSysPrompt))
|
||||||
// Register websearch agent
|
// Register websearch agent
|
||||||
agent.Register("websearch", agent.NewWebAgentB(client, webSearchSysPrompt))
|
agent.RegisterB("websearch", agent.NewWebAgentB(client, webSearchSysPrompt))
|
||||||
// Register read_url agent
|
// Register read_url agent
|
||||||
agent.Register("read_url", agent.NewWebAgentB(client, readURLSysPrompt))
|
agent.RegisterB("read_url", agent.NewWebAgentB(client, readURLSysPrompt))
|
||||||
// Register summarize_chat agent
|
// Register summarize_chat agent
|
||||||
agent.Register("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt))
|
agent.RegisterB("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
118
tui.go
118
tui.go
@@ -29,8 +29,6 @@ var (
|
|||||||
statusLineWidget *tview.TextView
|
statusLineWidget *tview.TextView
|
||||||
helpView *tview.TextView
|
helpView *tview.TextView
|
||||||
flex *tview.Flex
|
flex *tview.Flex
|
||||||
bottomFlex *tview.Flex
|
|
||||||
notificationWidget *tview.TextView
|
|
||||||
imgView *tview.Image
|
imgView *tview.Image
|
||||||
defaultImage = "sysprompts/llama.png"
|
defaultImage = "sysprompts/llama.png"
|
||||||
indexPickWindow *tview.InputField
|
indexPickWindow *tview.InputField
|
||||||
@@ -38,7 +36,6 @@ var (
|
|||||||
roleEditWindow *tview.InputField
|
roleEditWindow *tview.InputField
|
||||||
shellInput *tview.InputField
|
shellInput *tview.InputField
|
||||||
confirmModal *tview.Modal
|
confirmModal *tview.Modal
|
||||||
toastTimer *time.Timer
|
|
||||||
confirmPageName = "confirm"
|
confirmPageName = "confirm"
|
||||||
fullscreenMode bool
|
fullscreenMode bool
|
||||||
positionVisible bool = true
|
positionVisible bool = true
|
||||||
@@ -140,8 +137,8 @@ func setShellMode(enabled bool) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// showToast displays a temporary notification in the bottom-right corner.
|
// showToast displays a temporary message in the top‑right corner.
|
||||||
// It auto-hides after 3 seconds.
|
// It auto‑hides after 3 seconds and disappears when clicked.
|
||||||
func showToast(title, message string) {
|
func showToast(title, message string) {
|
||||||
sanitize := func(s string, maxLen int) string {
|
sanitize := func(s string, maxLen int) string {
|
||||||
sanitized := strings.Map(func(r rune) rune {
|
sanitized := strings.Map(func(r rune) rune {
|
||||||
@@ -157,68 +154,33 @@ func showToast(title, message string) {
|
|||||||
}
|
}
|
||||||
title = sanitize(title, 50)
|
title = sanitize(title, 50)
|
||||||
message = sanitize(message, 197)
|
message = sanitize(message, 197)
|
||||||
if toastTimer != nil {
|
notification := tview.NewTextView().
|
||||||
toastTimer.Stop()
|
SetTextAlign(tview.AlignCenter).
|
||||||
}
|
SetDynamicColors(true).
|
||||||
// show blocking notification to not mess up flex
|
SetRegions(true).
|
||||||
if fullscreenMode {
|
SetText(fmt.Sprintf("[yellow]%s[-]\n", message)).
|
||||||
notification := tview.NewTextView().
|
SetChangedFunc(func() {
|
||||||
SetTextAlign(tview.AlignCenter).
|
app.Draw()
|
||||||
SetDynamicColors(true).
|
|
||||||
SetRegions(true).
|
|
||||||
SetText(fmt.Sprintf("[yellow]%s[-]\n", message)).
|
|
||||||
SetChangedFunc(func() {
|
|
||||||
app.Draw()
|
|
||||||
})
|
|
||||||
notification.SetTitleAlign(tview.AlignLeft).
|
|
||||||
SetBorder(true).
|
|
||||||
SetTitle(title)
|
|
||||||
// Wrap it in a full‑screen Flex to position it in the top‑right corner.
|
|
||||||
// Outer Flex (row) pushes content to the top; inner Flex (column) pushes to the right.
|
|
||||||
background := tview.NewFlex().SetDirection(tview.FlexRow).
|
|
||||||
AddItem(nil, 0, 1, false). // top spacer
|
|
||||||
AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
|
|
||||||
AddItem(nil, 0, 1, false). // left spacer
|
|
||||||
AddItem(notification, 40, 1, true), // notification width 40
|
|
||||||
5, 1, false) // notification height 5
|
|
||||||
// Generate a unique page name (e.g., using timestamp) to allow multiple toasts.
|
|
||||||
pageName := fmt.Sprintf("toast-%d", time.Now().UnixNano())
|
|
||||||
pages.AddPage(pageName, background, true, true)
|
|
||||||
// Auto‑dismiss after 2 seconds, since blocking is more annoying
|
|
||||||
time.AfterFunc(2*time.Second, func() {
|
|
||||||
app.QueueUpdateDraw(func() {
|
|
||||||
if pages.HasPage(pageName) {
|
|
||||||
pages.RemovePage(pageName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
return
|
notification.SetTitleAlign(tview.AlignLeft).
|
||||||
}
|
SetBorder(true).
|
||||||
notificationWidget.SetTitle(title)
|
SetTitle(title)
|
||||||
notificationWidget.SetText(fmt.Sprintf("[yellow]%s[-]", message))
|
// Wrap it in a full‑screen Flex to position it in the top‑right corner.
|
||||||
go func() {
|
// Outer Flex (row) pushes content to the top; inner Flex (column) pushes to the right.
|
||||||
|
background := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||||
|
AddItem(nil, 0, 1, false). // top spacer
|
||||||
|
AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
|
||||||
|
AddItem(nil, 0, 1, false). // left spacer
|
||||||
|
AddItem(notification, 40, 1, true), // notification width 40
|
||||||
|
5, 1, false) // notification height 5
|
||||||
|
// Generate a unique page name (e.g., using timestamp) to allow multiple toasts.
|
||||||
|
pageName := fmt.Sprintf("toast-%d", time.Now().UnixNano())
|
||||||
|
pages.AddPage(pageName, background, true, true)
|
||||||
|
// Auto‑dismiss after 3 seconds.
|
||||||
|
time.AfterFunc(3*time.Second, func() {
|
||||||
app.QueueUpdateDraw(func() {
|
app.QueueUpdateDraw(func() {
|
||||||
flex.RemoveItem(bottomFlex)
|
if pages.HasPage(pageName) {
|
||||||
flex.RemoveItem(statusLineWidget)
|
pages.RemovePage(pageName)
|
||||||
bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn).
|
|
||||||
AddItem(textArea, 0, 1, true).
|
|
||||||
AddItem(notificationWidget, 40, 1, false)
|
|
||||||
flex.AddItem(bottomFlex, 0, 10, true)
|
|
||||||
if positionVisible {
|
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
toastTimer = time.AfterFunc(3*time.Second, func() {
|
|
||||||
app.QueueUpdateDraw(func() {
|
|
||||||
flex.RemoveItem(bottomFlex)
|
|
||||||
flex.RemoveItem(statusLineWidget)
|
|
||||||
bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn).
|
|
||||||
AddItem(textArea, 0, 1, true).
|
|
||||||
AddItem(notificationWidget, 0, 0, false)
|
|
||||||
flex.AddItem(bottomFlex, 0, 10, true)
|
|
||||||
if positionVisible {
|
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -273,7 +235,7 @@ func init() {
|
|||||||
shellHistoryPos = -1
|
shellHistoryPos = -1
|
||||||
}
|
}
|
||||||
// Handle Tab key for @ file completion
|
// Handle Tab key for @ file completion
|
||||||
if event.Key() == tcell.KeyTab && shellMode {
|
if event.Key() == tcell.KeyTab {
|
||||||
currentText := shellInput.GetText()
|
currentText := shellInput.GetText()
|
||||||
atIndex := strings.LastIndex(currentText, "@")
|
atIndex := strings.LastIndex(currentText, "@")
|
||||||
if atIndex >= 0 {
|
if atIndex >= 0 {
|
||||||
@@ -324,26 +286,12 @@ func init() {
|
|||||||
SetDynamicColors(true).
|
SetDynamicColors(true).
|
||||||
SetRegions(true).
|
SetRegions(true).
|
||||||
SetChangedFunc(func() {
|
SetChangedFunc(func() {
|
||||||
// INFO:
|
|
||||||
// https://github.com/rivo/tview/wiki/Concurrency#event-handlers
|
|
||||||
// although already called by default per tview specs
|
|
||||||
// calling it explicitly makes text streaming to look more smooth
|
|
||||||
app.Draw()
|
app.Draw()
|
||||||
})
|
})
|
||||||
notificationWidget = tview.NewTextView().
|
|
||||||
SetTextAlign(tview.AlignCenter).
|
|
||||||
SetDynamicColors(true).
|
|
||||||
SetRegions(true).
|
|
||||||
SetChangedFunc(func() {
|
|
||||||
})
|
|
||||||
notificationWidget.SetBorder(true).SetTitle("notification")
|
|
||||||
bottomFlex = tview.NewFlex().SetDirection(tview.FlexColumn).
|
|
||||||
AddItem(textArea, 0, 1, true).
|
|
||||||
AddItem(notificationWidget, 0, 0, false)
|
|
||||||
//
|
//
|
||||||
flex = tview.NewFlex().SetDirection(tview.FlexRow).
|
flex = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||||
AddItem(textView, 0, 40, false).
|
AddItem(textView, 0, 40, false).
|
||||||
AddItem(bottomFlex, 0, 10, true)
|
AddItem(textArea, 0, 10, true) // Restore original height
|
||||||
if positionVisible {
|
if positionVisible {
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
flex.AddItem(statusLineWidget, 0, 2, false)
|
||||||
}
|
}
|
||||||
@@ -412,14 +360,10 @@ func init() {
|
|||||||
// y += h / 2
|
// y += h / 2
|
||||||
// return x, y, w, h
|
// return x, y, w, h
|
||||||
// })
|
// })
|
||||||
notificationWidget.SetDrawFunc(func(screen tcell.Screen, x, y, w, h int) (int, int, int, int) {
|
|
||||||
y += h / 2
|
|
||||||
return x, y, w, h
|
|
||||||
})
|
|
||||||
// Initially set up flex without search bar
|
// Initially set up flex without search bar
|
||||||
flex = tview.NewFlex().SetDirection(tview.FlexRow).
|
flex = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||||
AddItem(textView, 0, 40, false).
|
AddItem(textView, 0, 40, false).
|
||||||
AddItem(bottomFlex, 0, 10, true)
|
AddItem(textArea, 0, 10, true) // Restore original height
|
||||||
if positionVisible {
|
if positionVisible {
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
flex.AddItem(statusLineWidget, 0, 2, false)
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1095,7 @@ func init() {
|
|||||||
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
|
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyTab && !shellMode {
|
if event.Key() == tcell.KeyTab {
|
||||||
currentF := app.GetFocus()
|
currentF := app.GetFocus()
|
||||||
if currentF == textArea {
|
if currentF == textArea {
|
||||||
currentText := textArea.GetText()
|
currentText := textArea.GetText()
|
||||||
|
|||||||
Reference in New Issue
Block a user