Compare commits
11 Commits
feat/reaso
...
feat/resp-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef53e9bebe | ||
|
|
a546bfe596 | ||
|
|
23c21f87bb | ||
|
|
850ca103e5 | ||
|
|
b7b5fcbf79 | ||
|
|
1e13c7796d | ||
|
|
9a727b21ad | ||
|
|
beb944c390 | ||
|
|
5844dd1494 | ||
|
|
84c4010213 | ||
|
|
86260e218c |
@@ -8,7 +8,7 @@ made with use of [tview](https://github.com/rivo/tview)
|
|||||||
- tts/stt (run make commands to get deps);
|
- tts/stt (run make commands to get deps);
|
||||||
- image input;
|
- image input;
|
||||||
- function calls (function calls are implemented natively, to avoid calling outside sources);
|
- function calls (function calls are implemented natively, to avoid calling outside sources);
|
||||||
- [character specific context (unique feature)](char-specific-context.md)
|
- [character specific context (unique feature)](docs/char-specific-context.md)
|
||||||
|
|
||||||
#### how it looks
|
#### how it looks
|
||||||

|

|
||||||
|
|||||||
69
bot.go
69
bot.go
@@ -46,6 +46,7 @@ var (
|
|||||||
ragger *rag.RAG
|
ragger *rag.RAG
|
||||||
chunkParser ChunkParser
|
chunkParser ChunkParser
|
||||||
lastToolCall *models.FuncCall
|
lastToolCall *models.FuncCall
|
||||||
|
lastRespStats *models.ResponseStats
|
||||||
//nolint:unused // TTS_ENABLED conditionally uses this
|
//nolint:unused // TTS_ENABLED conditionally uses this
|
||||||
orator Orator
|
orator Orator
|
||||||
asr STT
|
asr STT
|
||||||
@@ -413,7 +414,7 @@ func fetchLCPModelsWithLoadStatus() ([]string, error) {
|
|||||||
for _, m := range models.Data {
|
for _, m := range models.Data {
|
||||||
modelName := m.ID
|
modelName := m.ID
|
||||||
if m.Status.Value == "loaded" {
|
if m.Status.Value == "loaded" {
|
||||||
modelName = modelName + " (loaded)"
|
modelName = "(loaded) " + modelName
|
||||||
}
|
}
|
||||||
result = append(result, modelName)
|
result = append(result, modelName)
|
||||||
}
|
}
|
||||||
@@ -484,30 +485,28 @@ func monitorModelLoad(modelID string) {
|
|||||||
// extractDetailedErrorFromBytes extracts detailed error information from response body bytes
|
// extractDetailedErrorFromBytes extracts detailed error information from response body bytes
|
||||||
func extractDetailedErrorFromBytes(body []byte, statusCode int) string {
|
func extractDetailedErrorFromBytes(body []byte, statusCode int) string {
|
||||||
// Try to parse as JSON to extract detailed error information
|
// Try to parse as JSON to extract detailed error information
|
||||||
var errorResponse map[string]interface{}
|
var errorResponse map[string]any
|
||||||
if err := json.Unmarshal(body, &errorResponse); err == nil {
|
if err := json.Unmarshal(body, &errorResponse); err == nil {
|
||||||
// Check if it's an error response with detailed information
|
// Check if it's an error response with detailed information
|
||||||
if errorData, ok := errorResponse["error"]; ok {
|
if errorData, ok := errorResponse["error"]; ok {
|
||||||
if errorMap, ok := errorData.(map[string]interface{}); ok {
|
if errorMap, ok := errorData.(map[string]any); ok {
|
||||||
var errorMsg string
|
var errorMsg string
|
||||||
if msg, ok := errorMap["message"]; ok {
|
if msg, ok := errorMap["message"]; ok {
|
||||||
errorMsg = fmt.Sprintf("%v", msg)
|
errorMsg = fmt.Sprintf("%v", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
var details []string
|
var details []string
|
||||||
if code, ok := errorMap["code"]; ok {
|
if code, ok := errorMap["code"]; ok {
|
||||||
details = append(details, fmt.Sprintf("Code: %v", code))
|
details = append(details, fmt.Sprintf("Code: %v", code))
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata, ok := errorMap["metadata"]; ok {
|
if metadata, ok := errorMap["metadata"]; ok {
|
||||||
// Handle metadata which might contain raw error details
|
// Handle metadata which might contain raw error details
|
||||||
if metadataMap, ok := metadata.(map[string]interface{}); ok {
|
if metadataMap, ok := metadata.(map[string]any); ok {
|
||||||
if raw, ok := metadataMap["raw"]; ok {
|
if raw, ok := metadataMap["raw"]; ok {
|
||||||
// Parse the raw error string if it's JSON
|
// Parse the raw error string if it's JSON
|
||||||
var rawError map[string]interface{}
|
var rawError map[string]any
|
||||||
if rawStr, ok := raw.(string); ok && json.Unmarshal([]byte(rawStr), &rawError) == nil {
|
if rawStr, ok := raw.(string); ok && json.Unmarshal([]byte(rawStr), &rawError) == nil {
|
||||||
if rawErrorData, ok := rawError["error"]; ok {
|
if rawErrorData, ok := rawError["error"]; ok {
|
||||||
if rawErrorMap, ok := rawErrorData.(map[string]interface{}); ok {
|
if rawErrorMap, ok := rawErrorData.(map[string]any); ok {
|
||||||
if rawMsg, ok := rawErrorMap["message"]; ok {
|
if rawMsg, ok := rawErrorMap["message"]; ok {
|
||||||
return fmt.Sprintf("API Error: %s", rawMsg)
|
return fmt.Sprintf("API Error: %s", rawMsg)
|
||||||
}
|
}
|
||||||
@@ -518,20 +517,30 @@ func extractDetailedErrorFromBytes(body []byte, statusCode int) string {
|
|||||||
}
|
}
|
||||||
details = append(details, fmt.Sprintf("Metadata: %v", metadata))
|
details = append(details, fmt.Sprintf("Metadata: %v", metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(details) > 0 {
|
if len(details) > 0 {
|
||||||
return fmt.Sprintf("API Error: %s (%s)", errorMsg, strings.Join(details, ", "))
|
return fmt.Sprintf("API Error: %s (%s)", errorMsg, strings.Join(details, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
return "API Error: " + errorMsg
|
return "API Error: " + errorMsg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not a structured error response, return the raw body with status
|
// If not a structured error response, return the raw body with status
|
||||||
return fmt.Sprintf("HTTP Status: %d, Response Body: %s", statusCode, string(body))
|
return fmt.Sprintf("HTTP Status: %d, Response Body: %s", statusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func finalizeRespStats(tokenCount int, startTime time.Time) {
|
||||||
|
duration := time.Since(startTime).Seconds()
|
||||||
|
var tps float64
|
||||||
|
if duration > 0 {
|
||||||
|
tps = float64(tokenCount) / duration
|
||||||
|
}
|
||||||
|
lastRespStats = &models.ResponseStats{
|
||||||
|
Tokens: tokenCount,
|
||||||
|
Duration: duration,
|
||||||
|
TokensPerSec: tps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sendMsgToLLM expects streaming resp
|
// sendMsgToLLM expects streaming resp
|
||||||
func sendMsgToLLM(body io.Reader) {
|
func sendMsgToLLM(body io.Reader) {
|
||||||
choseChunkParser()
|
choseChunkParser()
|
||||||
@@ -586,12 +595,17 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
streamDone <- true
|
streamDone <- true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
reader := bufio.NewReader(resp.Body)
|
reader := bufio.NewReader(resp.Body)
|
||||||
counter := uint32(0)
|
counter := uint32(0)
|
||||||
|
tokenCount := 0
|
||||||
|
startTime := time.Now()
|
||||||
hasReasoning := false
|
hasReasoning := false
|
||||||
reasoningSent := false
|
reasoningSent := false
|
||||||
|
defer func() {
|
||||||
|
finalizeRespStats(tokenCount, startTime)
|
||||||
|
}()
|
||||||
for {
|
for {
|
||||||
var (
|
var (
|
||||||
answerText string
|
answerText string
|
||||||
@@ -667,11 +681,13 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
// Close the thinking block if we were streaming reasoning and haven't closed it yet
|
// Close the thinking block if we were streaming reasoning and haven't closed it yet
|
||||||
if hasReasoning && !reasoningSent {
|
if hasReasoning && !reasoningSent {
|
||||||
chunkChan <- "</think>"
|
chunkChan <- "</think>"
|
||||||
|
tokenCount++
|
||||||
}
|
}
|
||||||
if chunk.Chunk != "" {
|
if chunk.Chunk != "" {
|
||||||
logger.Warn("text inside of finish llmchunk", "chunk", chunk, "counter", counter)
|
logger.Warn("text inside of finish llmchunk", "chunk", chunk, "counter", counter)
|
||||||
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
|
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
|
||||||
chunkChan <- answerText
|
chunkChan <- answerText
|
||||||
|
tokenCount++
|
||||||
}
|
}
|
||||||
streamDone <- true
|
streamDone <- true
|
||||||
break
|
break
|
||||||
@@ -684,12 +700,14 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
if !hasReasoning {
|
if !hasReasoning {
|
||||||
// First reasoning chunk - send opening tag
|
// First reasoning chunk - send opening tag
|
||||||
chunkChan <- "<think>"
|
chunkChan <- "<think>"
|
||||||
|
tokenCount++
|
||||||
hasReasoning = true
|
hasReasoning = true
|
||||||
}
|
}
|
||||||
// Stream reasoning content immediately
|
// Stream reasoning content immediately
|
||||||
answerText = strings.ReplaceAll(chunk.Reasoning, "\n\n", "\n")
|
answerText = strings.ReplaceAll(chunk.Reasoning, "\n\n", "\n")
|
||||||
if answerText != "" {
|
if answerText != "" {
|
||||||
chunkChan <- answerText
|
chunkChan <- answerText
|
||||||
|
tokenCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +715,7 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
if chunk.Chunk != "" && hasReasoning && !reasoningSent {
|
if chunk.Chunk != "" && hasReasoning && !reasoningSent {
|
||||||
// Close the thinking block before sending actual content
|
// Close the thinking block before sending actual content
|
||||||
chunkChan <- "</think>"
|
chunkChan <- "</think>"
|
||||||
|
tokenCount++
|
||||||
reasoningSent = true
|
reasoningSent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,9 +728,11 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
slices.Contains(stopStrings, answerText) {
|
slices.Contains(stopStrings, answerText) {
|
||||||
logger.Debug("stop string detected on client side for completion endpoint", "stop_string", answerText)
|
logger.Debug("stop string detected on client side for completion endpoint", "stop_string", answerText)
|
||||||
streamDone <- true
|
streamDone <- true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if answerText != "" {
|
if answerText != "" {
|
||||||
chunkChan <- answerText
|
chunkChan <- answerText
|
||||||
|
tokenCount++
|
||||||
}
|
}
|
||||||
openAIToolChan <- chunk.ToolChunk
|
openAIToolChan <- chunk.ToolChunk
|
||||||
if chunk.FuncName != "" {
|
if chunk.FuncName != "" {
|
||||||
@@ -914,7 +935,6 @@ out:
|
|||||||
textView.ScrollToEnd()
|
textView.ScrollToEnd()
|
||||||
}
|
}
|
||||||
case <-streamDone:
|
case <-streamDone:
|
||||||
// drain any remaining chunks from chunkChan before exiting
|
|
||||||
for len(chunkChan) > 0 {
|
for len(chunkChan) > 0 {
|
||||||
chunk := <-chunkChan
|
chunk := <-chunkChan
|
||||||
fmt.Fprint(textView, chunk)
|
fmt.Fprint(textView, chunk)
|
||||||
@@ -923,31 +943,40 @@ out:
|
|||||||
textView.ScrollToEnd()
|
textView.ScrollToEnd()
|
||||||
}
|
}
|
||||||
if cfg.TTS_ENABLED {
|
if cfg.TTS_ENABLED {
|
||||||
// Send chunk to audio stream handler
|
|
||||||
TTSTextChan <- chunk
|
TTSTextChan <- chunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.TTS_ENABLED {
|
if cfg.TTS_ENABLED {
|
||||||
// msg is done; flush it down
|
|
||||||
TTSFlushChan <- true
|
TTSFlushChan <- true
|
||||||
}
|
}
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var msgStats *models.ResponseStats
|
||||||
|
if lastRespStats != nil {
|
||||||
|
msgStats = &models.ResponseStats{
|
||||||
|
Tokens: lastRespStats.Tokens,
|
||||||
|
Duration: lastRespStats.Duration,
|
||||||
|
TokensPerSec: lastRespStats.TokensPerSec,
|
||||||
|
}
|
||||||
|
lastRespStats = nil
|
||||||
|
}
|
||||||
botRespMode = false
|
botRespMode = false
|
||||||
// numbers in chatbody and displayed must be the same
|
|
||||||
if r.Resume {
|
if r.Resume {
|
||||||
chatBody.Messages[len(chatBody.Messages)-1].Content += respText.String()
|
chatBody.Messages[len(chatBody.Messages)-1].Content += respText.String()
|
||||||
// lastM.Content = lastM.Content + respText.String()
|
|
||||||
// Process the updated message to check for known_to tags in resumed response
|
|
||||||
updatedMsg := chatBody.Messages[len(chatBody.Messages)-1]
|
updatedMsg := chatBody.Messages[len(chatBody.Messages)-1]
|
||||||
processedMsg := processMessageTag(&updatedMsg)
|
processedMsg := processMessageTag(&updatedMsg)
|
||||||
chatBody.Messages[len(chatBody.Messages)-1] = *processedMsg
|
chatBody.Messages[len(chatBody.Messages)-1] = *processedMsg
|
||||||
|
if msgStats != nil && chatBody.Messages[len(chatBody.Messages)-1].Role != cfg.ToolRole {
|
||||||
|
chatBody.Messages[len(chatBody.Messages)-1].Stats = msgStats
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Message was already added at the start, just process it for known_to tags
|
|
||||||
chatBody.Messages[msgIdx].Content = respText.String()
|
chatBody.Messages[msgIdx].Content = respText.String()
|
||||||
processedMsg := processMessageTag(&chatBody.Messages[msgIdx])
|
processedMsg := processMessageTag(&chatBody.Messages[msgIdx])
|
||||||
chatBody.Messages[msgIdx] = *processedMsg
|
chatBody.Messages[msgIdx] = *processedMsg
|
||||||
|
if msgStats != nil && chatBody.Messages[msgIdx].Role != cfg.ToolRole {
|
||||||
|
chatBody.Messages[msgIdx].Stats = msgStats
|
||||||
|
}
|
||||||
stopTTSIfNotForUser(&chatBody.Messages[msgIdx])
|
stopTTSIfNotForUser(&chatBody.Messages[msgIdx])
|
||||||
}
|
}
|
||||||
cleanChatBody()
|
cleanChatBody()
|
||||||
@@ -1369,7 +1398,7 @@ func init() {
|
|||||||
var err error
|
var err error
|
||||||
cfg, err = config.LoadConfig("config.toml")
|
cfg, err = config.LoadConfig("config.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("failed to load config.toml")
|
fmt.Println("failed to load config.toml", err)
|
||||||
cancel()
|
cancel()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ DeepSeekModel = "deepseek-reasoner"
|
|||||||
OpenRouterCompletionAPI = "https://openrouter.ai/api/v1/completions"
|
OpenRouterCompletionAPI = "https://openrouter.ai/api/v1/completions"
|
||||||
OpenRouterChatAPI = "https://openrouter.ai/api/v1/chat/completions"
|
OpenRouterChatAPI = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
# OpenRouterToken = ""
|
# OpenRouterToken = ""
|
||||||
|
# embeddings
|
||||||
EmbedURL = "http://localhost:8082/v1/embeddings"
|
EmbedURL = "http://localhost:8082/v1/embeddings"
|
||||||
|
HFToken = false
|
||||||
|
#
|
||||||
ShowSys = true
|
ShowSys = true
|
||||||
LogFile = "log.txt"
|
LogFile = "log.txt"
|
||||||
UserRole = "user"
|
UserRole = "user"
|
||||||
@@ -21,6 +24,7 @@ ChunkLimit = 100000
|
|||||||
AutoScrollEnabled = true
|
AutoScrollEnabled = true
|
||||||
AutoCleanToolCallsFromCtx = false
|
AutoCleanToolCallsFromCtx = false
|
||||||
# rag settings
|
# rag settings
|
||||||
|
RAGEnabled = false
|
||||||
RAGBatchSize = 1
|
RAGBatchSize = 1
|
||||||
RAGWordLimit = 80
|
RAGWordLimit = 80
|
||||||
RAGWorkers = 2
|
RAGWorkers = 2
|
||||||
@@ -41,9 +45,8 @@ STT_LANG = "en" # Language for speech recognition (for WHISPER_BINARY mode)
|
|||||||
STT_SR = 16000 # Sample rate for audio recording
|
STT_SR = 16000 # Sample rate for audio recording
|
||||||
#
|
#
|
||||||
DBPATH = "gflt.db"
|
DBPATH = "gflt.db"
|
||||||
FilePickerDir = "." # Directory where file picker should start
|
FilePickerDir = "." # Directory for file picker start and coding assistant file operations (relative paths resolved against this)
|
||||||
FilePickerExts = "png,jpg,jpeg,gif,webp" # Comma-separated list of allowed file extensions for file picker
|
FilePickerExts = "png,jpg,jpeg,gif,webp" # Comma-separated list of allowed file extensions for file picker
|
||||||
CodingDir = "." # Default directory for coding assistant file operations (relative paths resolved against this)
|
|
||||||
EnableMouse = false # Enable mouse support in the UI
|
EnableMouse = false # Enable mouse support in the UI
|
||||||
# character specific context
|
# character specific context
|
||||||
CharSpecificContextEnabled = true
|
CharSpecificContextEnabled = true
|
||||||
|
|||||||
@@ -32,15 +32,14 @@ type Config struct {
|
|||||||
DBPATH string `toml:"DBPATH"`
|
DBPATH string `toml:"DBPATH"`
|
||||||
FilePickerDir string `toml:"FilePickerDir"`
|
FilePickerDir string `toml:"FilePickerDir"`
|
||||||
FilePickerExts string `toml:"FilePickerExts"`
|
FilePickerExts string `toml:"FilePickerExts"`
|
||||||
CodingDir string `toml:"CodingDir"`
|
|
||||||
ImagePreview bool `toml:"ImagePreview"`
|
ImagePreview bool `toml:"ImagePreview"`
|
||||||
EnableMouse bool `toml:"EnableMouse"`
|
EnableMouse bool `toml:"EnableMouse"`
|
||||||
// embeddings
|
// embeddings
|
||||||
RAGEnabled bool `toml:"RAGEnabled"`
|
EmbedURL string `toml:"EmbedURL"`
|
||||||
EmbedURL string `toml:"EmbedURL"`
|
HFToken string `toml:"HFToken"`
|
||||||
HFToken string `toml:"HFToken"`
|
|
||||||
RAGDir string `toml:"RAGDir"`
|
|
||||||
// rag settings
|
// rag settings
|
||||||
|
RAGEnabled bool `toml:"RAGEnabled"`
|
||||||
|
RAGDir string `toml:"RAGDir"`
|
||||||
RAGWorkers uint32 `toml:"RAGWorkers"`
|
RAGWorkers uint32 `toml:"RAGWorkers"`
|
||||||
RAGBatchSize int `toml:"RAGBatchSize"`
|
RAGBatchSize int `toml:"RAGBatchSize"`
|
||||||
RAGWordLimit uint32 `toml:"RAGWordLimit"`
|
RAGWordLimit uint32 `toml:"RAGWordLimit"`
|
||||||
|
|||||||
@@ -140,17 +140,24 @@ This document explains how to set up and configure the application using the `co
|
|||||||
- Path to the SQLite database file used for storing conversation history and other data.
|
- Path to the SQLite database file used for storing conversation history and other data.
|
||||||
|
|
||||||
#### FilePickerDir (`"."`)
|
#### FilePickerDir (`"."`)
|
||||||
- Directory where the file (image) picker should start when selecting files.
|
- Directory where the file picker starts and where relative paths in coding assistant file tools (file_read, file_write, etc.) are resolved against. Use absolute paths (starting with `/`) to bypass this.
|
||||||
|
|
||||||
#### FilePickerExts (`"png,jpg,jpeg,gif,webp"`)
|
|
||||||
- Comma-separated list of allowed file extensions for the file picker.
|
|
||||||
|
|
||||||
#### CodingDir (`"."`)
|
|
||||||
- Default directory for coding assistant file operations. Relative paths in file tools (file_read, file_write, etc.) are resolved against this directory. Use absolute paths (starting with `/`) to bypass this.
|
|
||||||
|
|
||||||
#### EnableMouse (`false`)
|
#### EnableMouse (`false`)
|
||||||
- Enable or disable mouse support in the UI. When set to `true`, allows clicking buttons and interacting with UI elements using the mouse, but prevents the terminal from handling mouse events normally (such as selecting and copying text). When set to `false`, enables default terminal behavior allowing you to select and copy text, but disables mouse interaction with UI elements.
|
- Enable or disable mouse support in the UI. When set to `true`, allows clicking buttons and interacting with UI elements using the mouse, but prevents the terminal from handling mouse events normally (such as selecting and copying text). When set to `false`, enables default terminal behavior allowing you to select and copy text, but disables mouse interaction with UI elements.
|
||||||
|
|
||||||
|
### Character-Specific Context Settings (/completion only)
|
||||||
|
|
||||||
|
[character specific context page for more info](./char-specific-context.md)
|
||||||
|
|
||||||
|
#### CharSpecificContextEnabled (`true`)
|
||||||
|
- Enable or disable character-specific context functionality.
|
||||||
|
|
||||||
|
#### CharSpecificContextTag (`"@"`)
|
||||||
|
- The tag prefix used to reference character-specific context in messages.
|
||||||
|
|
||||||
|
#### AutoTurn (`true`)
|
||||||
|
- Enable or disable automatic turn detection/switching.
|
||||||
|
|
||||||
### Additional Features
|
### Additional Features
|
||||||
|
|
||||||
Those could be switched in program, but also bould be setup in config.
|
Those could be switched in program, but also bould be setup in config.
|
||||||
@@ -159,7 +166,13 @@ Those could be switched in program, but also bould be setup in config.
|
|||||||
- Enable or disable explanation of tools to llm, so it could use them.
|
- Enable or disable explanation of tools to llm, so it could use them.
|
||||||
|
|
||||||
#### ThinkUse
|
#### ThinkUse
|
||||||
- Enable or disable insertion of <think> token at the beggining of llm resp.
|
- Enable or disable insertion of JsonSerializerToken at the beggining of llm resp.
|
||||||
|
|
||||||
|
### StripThinkingFromAPI (`true`)
|
||||||
|
- Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls.
|
||||||
|
|
||||||
|
#### ReasoningEffort (`"medium"`)
|
||||||
|
- OpenRouter reasoning configuration (only applies to OpenRouter chat API). Valid values: `xhigh`, `high`, `medium`, `low`, `minimal`, `none`. Empty or `none` disables reasoning.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
|
|||||||
62
extra/tts.go
62
extra/tts.go
@@ -92,8 +92,6 @@ func (o *KokoroOrator) stoproutine() {
|
|||||||
|
|
||||||
func (o *KokoroOrator) readroutine() {
|
func (o *KokoroOrator) readroutine() {
|
||||||
tokenizer, _ := english.NewSentenceTokenizer(nil)
|
tokenizer, _ := english.NewSentenceTokenizer(nil)
|
||||||
// var sentenceBuf bytes.Buffer
|
|
||||||
// var remainder strings.Builder
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case chunk := <-TTSTextChan:
|
case chunk := <-TTSTextChan:
|
||||||
@@ -106,24 +104,28 @@ func (o *KokoroOrator) readroutine() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := o.textBuffer.String()
|
text := o.textBuffer.String()
|
||||||
o.mu.Unlock()
|
|
||||||
sentences := tokenizer.Tokenize(text)
|
sentences := tokenizer.Tokenize(text)
|
||||||
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
|
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
|
||||||
for i, sentence := range sentences {
|
if len(sentences) <= 1 {
|
||||||
if i == len(sentences)-1 { // last sentence
|
o.mu.Unlock()
|
||||||
o.mu.Lock()
|
continue
|
||||||
o.textBuffer.Reset()
|
}
|
||||||
_, err := o.textBuffer.WriteString(sentence.Text)
|
completeSentences := sentences[:len(sentences)-1]
|
||||||
o.mu.Unlock()
|
remaining := sentences[len(sentences)-1].Text
|
||||||
if err != nil {
|
o.textBuffer.Reset()
|
||||||
o.logger.Warn("failed to write to stringbuilder", "error", err)
|
o.textBuffer.WriteString(remaining)
|
||||||
continue
|
o.mu.Unlock()
|
||||||
}
|
|
||||||
continue // if only one (often incomplete) sentence; wait for next chunk
|
for _, sentence := range completeSentences {
|
||||||
|
o.mu.Lock()
|
||||||
|
interrupted := o.interrupt
|
||||||
|
o.mu.Unlock()
|
||||||
|
if interrupted {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
cleanedText := models.CleanText(sentence.Text)
|
cleanedText := models.CleanText(sentence.Text)
|
||||||
if cleanedText == "" {
|
if cleanedText == "" {
|
||||||
continue // Skip empty text after cleaning
|
continue
|
||||||
}
|
}
|
||||||
o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
|
o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
|
||||||
if err := o.Speak(cleanedText); err != nil {
|
if err := o.Speak(cleanedText); err != nil {
|
||||||
@@ -338,24 +340,28 @@ func (o *GoogleTranslateOrator) readroutine() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := o.textBuffer.String()
|
text := o.textBuffer.String()
|
||||||
o.mu.Unlock()
|
|
||||||
sentences := tokenizer.Tokenize(text)
|
sentences := tokenizer.Tokenize(text)
|
||||||
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
|
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
|
||||||
for i, sentence := range sentences {
|
if len(sentences) <= 1 {
|
||||||
if i == len(sentences)-1 { // last sentence
|
o.mu.Unlock()
|
||||||
o.mu.Lock()
|
continue
|
||||||
o.textBuffer.Reset()
|
}
|
||||||
_, err := o.textBuffer.WriteString(sentence.Text)
|
completeSentences := sentences[:len(sentences)-1]
|
||||||
o.mu.Unlock()
|
remaining := sentences[len(sentences)-1].Text
|
||||||
if err != nil {
|
o.textBuffer.Reset()
|
||||||
o.logger.Warn("failed to write to stringbuilder", "error", err)
|
o.textBuffer.WriteString(remaining)
|
||||||
continue
|
o.mu.Unlock()
|
||||||
}
|
|
||||||
continue // if only one (often incomplete) sentence; wait for next chunk
|
for _, sentence := range completeSentences {
|
||||||
|
o.mu.Lock()
|
||||||
|
interrupted := o.interrupt
|
||||||
|
o.mu.Unlock()
|
||||||
|
if interrupted {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
cleanedText := models.CleanText(sentence.Text)
|
cleanedText := models.CleanText(sentence.Text)
|
||||||
if cleanedText == "" {
|
if cleanedText == "" {
|
||||||
continue // Skip empty text after cleaning
|
continue
|
||||||
}
|
}
|
||||||
o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
|
o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
|
||||||
if err := o.Speak(cleanedText); err != nil {
|
if err := o.Speak(cleanedText); err != nil {
|
||||||
|
|||||||
21
helpfuncs.go
21
helpfuncs.go
@@ -108,14 +108,17 @@ func refreshChatDisplay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopTTSIfNotForUser: character specific context, not meant fot the human to hear
|
||||||
func stopTTSIfNotForUser(msg *models.RoleMsg) {
|
func stopTTSIfNotForUser(msg *models.RoleMsg) {
|
||||||
|
if strings.Contains(cfg.CurrentAPI, "/chat") || !cfg.CharSpecificContextEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
viewingAs := cfg.UserRole
|
viewingAs := cfg.UserRole
|
||||||
if cfg.WriteNextMsgAs != "" {
|
if cfg.WriteNextMsgAs != "" {
|
||||||
viewingAs = cfg.WriteNextMsgAs
|
viewingAs = cfg.WriteNextMsgAs
|
||||||
}
|
}
|
||||||
// stop tts if msg is not for user
|
// stop tts if msg is not for user
|
||||||
if cfg.CharSpecificContextEnabled &&
|
if !slices.Contains(msg.KnownTo, viewingAs) && cfg.TTS_ENABLED {
|
||||||
!slices.Contains(msg.KnownTo, viewingAs) && cfg.TTS_ENABLED {
|
|
||||||
TTSDoneChan <- true
|
TTSDoneChan <- true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,13 +357,17 @@ func makeStatusLine() string {
|
|||||||
}
|
}
|
||||||
// Get model color based on load status for local llama.cpp models
|
// Get model color based on load status for local llama.cpp models
|
||||||
modelColor := getModelColor()
|
modelColor := getModelColor()
|
||||||
statusLine := fmt.Sprintf(statusLineTempl, boolColors[botRespMode], botRespMode, activeChatName,
|
statusLine := fmt.Sprintf(statusLineTempl, boolColors[botRespMode], activeChatName,
|
||||||
boolColors[cfg.ToolUse], cfg.ToolUse, modelColor, chatBody.Model, boolColors[cfg.SkipLLMResp],
|
boolColors[cfg.ToolUse], modelColor, chatBody.Model, boolColors[cfg.SkipLLMResp],
|
||||||
cfg.SkipLLMResp, cfg.CurrentAPI, boolColors[isRecording], isRecording, persona,
|
cfg.CurrentAPI, persona, botPersona)
|
||||||
botPersona)
|
if cfg.STT_ENABLED {
|
||||||
|
recordingS := fmt.Sprintf(" | [%s:-:b]voice recording[-:-:-] (ctrl+r)",
|
||||||
|
boolColors[isRecording])
|
||||||
|
statusLine += recordingS
|
||||||
|
}
|
||||||
// completion endpoint
|
// completion endpoint
|
||||||
if !strings.Contains(cfg.CurrentAPI, "chat") {
|
if !strings.Contains(cfg.CurrentAPI, "chat") {
|
||||||
roleInject := fmt.Sprintf(" | role injection [%s:-:b]%v[-:-:-] (alt+7)", boolColors[injectRole], injectRole)
|
roleInject := fmt.Sprintf(" | [%s:-:b]role injection[-:-:-] (alt+7)", boolColors[injectRole])
|
||||||
statusLine += roleInject
|
statusLine += roleInject
|
||||||
}
|
}
|
||||||
return statusLine + imageInfo + shellModeInfo
|
return statusLine + imageInfo + shellModeInfo
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -13,7 +13,7 @@ var (
|
|||||||
selectedIndex = int(-1)
|
selectedIndex = int(-1)
|
||||||
shellMode = false
|
shellMode = false
|
||||||
thinkingCollapsed = false
|
thinkingCollapsed = false
|
||||||
statusLineTempl = "help (F12) | llm turn: [%s:-:b]%v[-:-:-] (F6) | chat: [orange:-:b]%s[-:-:-] (F1) |tool-use: [%s:-:b]%v[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [%s:-:b]%v[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | voice recording: [%s:-:b]%v[-:-:-] (ctrl+r) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
|
statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing 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{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -105,12 +105,13 @@ type ImageContentPart struct {
|
|||||||
|
|
||||||
// RoleMsg represents a message with content that can be either a simple string or structured content parts
|
// RoleMsg represents a message with content that can be either a simple string or structured content parts
|
||||||
type RoleMsg struct {
|
type RoleMsg struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"-"`
|
Content string `json:"-"`
|
||||||
ContentParts []any `json:"-"`
|
ContentParts []any `json:"-"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages
|
ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages
|
||||||
KnownTo []string `json:"known_to,omitempty"`
|
KnownTo []string `json:"known_to,omitempty"`
|
||||||
hasContentParts bool // Flag to indicate which content type to marshal
|
Stats *ResponseStats `json:"stats"`
|
||||||
|
hasContentParts bool // Flag to indicate which content type to marshal
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements custom JSON marshaling for RoleMsg
|
// MarshalJSON implements custom JSON marshaling for RoleMsg
|
||||||
@@ -183,13 +184,11 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RoleMsg) ToText(i int) string {
|
func (m *RoleMsg) ToText(i int) string {
|
||||||
// Convert content to string representation
|
|
||||||
var contentStr string
|
var contentStr string
|
||||||
var imageIndicators []string
|
var imageIndicators []string
|
||||||
if !m.hasContentParts {
|
if !m.hasContentParts {
|
||||||
contentStr = m.Content
|
contentStr = m.Content
|
||||||
} else {
|
} else {
|
||||||
// For structured content, collect text parts and image indicators
|
|
||||||
var textParts []string
|
var textParts []string
|
||||||
for _, part := range m.ContentParts {
|
for _, part := range m.ContentParts {
|
||||||
switch p := part.(type) {
|
switch p := part.(type) {
|
||||||
@@ -198,7 +197,6 @@ func (m *RoleMsg) ToText(i int) string {
|
|||||||
textParts = append(textParts, p.Text)
|
textParts = append(textParts, p.Text)
|
||||||
}
|
}
|
||||||
case ImageContentPart:
|
case ImageContentPart:
|
||||||
// Collect image indicator
|
|
||||||
displayPath := p.Path
|
displayPath := p.Path
|
||||||
if displayPath == "" {
|
if displayPath == "" {
|
||||||
displayPath = "image"
|
displayPath = "image"
|
||||||
@@ -216,7 +214,6 @@ func (m *RoleMsg) ToText(i int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "image_url":
|
case "image_url":
|
||||||
// Handle unmarshaled image content
|
|
||||||
var displayPath string
|
var displayPath string
|
||||||
if pathVal, pathExists := p["path"]; pathExists {
|
if pathVal, pathExists := p["path"]; pathExists {
|
||||||
if pathStr, isStr := pathVal.(string); isStr && pathStr != "" {
|
if pathStr, isStr := pathVal.(string); isStr && pathStr != "" {
|
||||||
@@ -233,23 +230,20 @@ func (m *RoleMsg) ToText(i int) string {
|
|||||||
}
|
}
|
||||||
contentStr = strings.Join(textParts, " ") + " "
|
contentStr = strings.Join(textParts, " ") + " "
|
||||||
}
|
}
|
||||||
// check if already has role annotation (/completion makes them)
|
|
||||||
// in that case remove it, and then add to icon
|
|
||||||
// since icon and content are separated by \n
|
|
||||||
contentStr, _ = strings.CutPrefix(contentStr, m.Role+":")
|
contentStr, _ = strings.CutPrefix(contentStr, m.Role+":")
|
||||||
// if !strings.HasPrefix(contentStr, m.Role+":") {
|
|
||||||
icon := fmt.Sprintf("(%d) <%s>: ", i, m.Role)
|
icon := fmt.Sprintf("(%d) <%s>: ", i, m.Role)
|
||||||
// }
|
|
||||||
// Build final message with image indicators before text
|
|
||||||
var finalContent strings.Builder
|
var finalContent strings.Builder
|
||||||
if len(imageIndicators) > 0 {
|
if len(imageIndicators) > 0 {
|
||||||
// Add each image indicator on its own line
|
|
||||||
for _, indicator := range imageIndicators {
|
for _, indicator := range imageIndicators {
|
||||||
finalContent.WriteString(indicator)
|
finalContent.WriteString(indicator)
|
||||||
finalContent.WriteString("\n")
|
finalContent.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalContent.WriteString(contentStr)
|
finalContent.WriteString(contentStr)
|
||||||
|
if m.Stats != nil {
|
||||||
|
finalContent.WriteString(fmt.Sprintf("\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]",
|
||||||
|
m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec))
|
||||||
|
}
|
||||||
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
|
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
|
||||||
return strings.ReplaceAll(textMsg, "\n\n", "\n")
|
return strings.ReplaceAll(textMsg, "\n\n", "\n")
|
||||||
}
|
}
|
||||||
@@ -331,6 +325,7 @@ func (m *RoleMsg) Copy() RoleMsg {
|
|||||||
ContentParts: m.ContentParts,
|
ContentParts: m.ContentParts,
|
||||||
ToolCallID: m.ToolCallID,
|
ToolCallID: m.ToolCallID,
|
||||||
KnownTo: m.KnownTo,
|
KnownTo: m.KnownTo,
|
||||||
|
Stats: m.Stats,
|
||||||
hasContentParts: m.hasContentParts,
|
hasContentParts: m.hasContentParts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,6 +638,12 @@ func (lcp *LCPModels) ListModels() []string {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResponseStats struct {
|
||||||
|
Tokens int
|
||||||
|
Duration float64
|
||||||
|
TokensPerSec float64
|
||||||
|
}
|
||||||
|
|
||||||
type ChatRoundReq struct {
|
type ChatRoundReq struct {
|
||||||
UserMsg string
|
UserMsg string
|
||||||
Role string
|
Role string
|
||||||
|
|||||||
12
popups.go
12
popups.go
@@ -61,14 +61,11 @@ func showModelSelectionPopup() {
|
|||||||
modelListWidget.SetCurrentItem(currentModelIndex)
|
modelListWidget.SetCurrentItem(currentModelIndex)
|
||||||
}
|
}
|
||||||
modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||||
// Strip "(loaded)" suffix if present for local llama.cpp models
|
modelName := strings.TrimPrefix(mainText, "(loaded) ")
|
||||||
modelName := strings.TrimSuffix(mainText, " (loaded)")
|
|
||||||
// Update the model in both chatBody and config
|
|
||||||
chatBody.Model = modelName
|
chatBody.Model = modelName
|
||||||
cfg.CurrentModel = chatBody.Model
|
cfg.CurrentModel = chatBody.Model
|
||||||
// Remove the popup page
|
|
||||||
pages.RemovePage("modelSelectionPopup")
|
pages.RemovePage("modelSelectionPopup")
|
||||||
// Update the status line to reflect the change
|
updateCachedModelColor()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
})
|
})
|
||||||
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
@@ -162,10 +159,9 @@ func showAPILinkSelectionPopup() {
|
|||||||
chatBody.Model = newModelList[0]
|
chatBody.Model = newModelList[0]
|
||||||
cfg.CurrentModel = chatBody.Model
|
cfg.CurrentModel = chatBody.Model
|
||||||
}
|
}
|
||||||
// Remove the popup page
|
|
||||||
pages.RemovePage("apiLinkSelectionPopup")
|
pages.RemovePage("apiLinkSelectionPopup")
|
||||||
// Update the parser and status line to reflect the change
|
|
||||||
choseChunkParser()
|
choseChunkParser()
|
||||||
|
updateCachedModelColor()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
})
|
})
|
||||||
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
@@ -336,7 +332,7 @@ func showBotRoleSelectionPopup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showFileCompletionPopup(filter string) {
|
func showFileCompletionPopup(filter string) {
|
||||||
baseDir := cfg.CodingDir
|
baseDir := cfg.FilePickerDir
|
||||||
if baseDir == "" {
|
if baseDir == "" {
|
||||||
baseDir = "."
|
baseDir = "."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sys_prompt": "You are an expert software engineering assistant. Your goal is to help users with coding tasks, debugging, refactoring, and software development.\n\n## Core Principles\n1. **Security First**: Never expose secrets, keys, or credentials. Never commit sensitive data.\n2. **No Git Actions**: You can READ git info (status, log, diff) for context, but NEVER perform git actions (commit, add, push, checkout, reset, rm, etc.). Let the user handle all git operations.\n3. **Explore Before Execute**: Always understand the codebase structure before making changes.\n4. **Follow Conventions**: Match existing code style, patterns, and frameworks used in the project.\n5. **Be Concise**: Minimize output tokens while maintaining quality. Avoid unnecessary explanations.\n\n## Workflow for Complex Tasks\nFor multi-step tasks, ALWAYS use the todo system to track progress:\n\n1. **Create Todo List**: At the start of complex tasks, use `todo_create` to break down work into actionable items.\n2. **Update Progress**: Mark items as `in_progress` when working on them, and `completed` when done.\n3. **Check Status**: Use `todo_read` to review your progress.\n\nExample workflow:\n- User: \"Add user authentication to this app\"\n- You: Create todos: [\"Analyze existing auth structure\", \"Check frameworks in use\", \"Implement auth middleware\", \"Add login endpoints\", \"Test implementation\"]\n\n## Task Execution Flow\n\n### Phase 1: Exploration (Always First)\n- Use `file_list` to understand directory structure (path defaults to CodingDir if not specified)\n- Use `file_read` to examine relevant files (paths are relative to CodingDir unless starting with `/`)\n- Use `execute_command` with `grep`/`find` to search for patterns\n- Check `README` or documentation files\n- Identify: frameworks, conventions, testing approach\n- **Git reads allowed**: You may use `git status`, `git log`, `git diff` for context, but only to inform your work\n- **Path handling**: Relative paths are resolved against CodingDir (configurable via Alt+O). Use absolute paths (starting with `/`) to bypass CodingDir.\n\n### Phase 2: Planning\n- For complex tasks: create todo items\n- Identify files that need modification\n- Plan your approach following existing patterns\n\n### Phase 3: Implementation\n- Make changes using appropriate file tools\n- Prefer `file_write` for new files, `file_read` then modify for existing files\n- Follow existing code style exactly\n- Use existing libraries and utilities\n\n### Phase 4: Verification\n- Run tests if available (check for test scripts)\n- Run linting/type checking commands\n- Verify changes work as expected\n\n### Phase 5: Completion\n- Update todos to `completed`\n- Provide concise summary of changes\n- Reference specific file paths and line numbers when relevant\n- **DO NOT commit changes** - inform user what was done so they can review and commit themselves\n\n## Tool Usage Guidelines\n\n**File Operations**:\n- `file_read`: Read before editing. Use for understanding code.\n- `file_write`: Overwrite file content completely.\n- `file_write_append`: Add to end of file.\n- `file_create`: Create new files with optional content.\n- `file_list`: List directory contents (defaults to CodingDir).\n- Paths are relative to CodingDir unless starting with `/`.\n\n**Command Execution (WHITELISTED ONLY)**:\n- Allowed: grep, sed, awk, find, cat, head, tail, sort, uniq, wc, ls, echo, cut, tr, cp, mv, rm, mkdir, rmdir, pwd, df, free, ps, top, du, whoami, date, uname\n- **Git reads allowed**: git status, git log, git diff, git show, git branch, git reflog, git rev-parse, git shortlog, git describe\n- **Git actions FORBIDDEN**: git add, git commit, git push, git checkout, git reset, git rm, etc.\n- Use for searching code, reading git context, running tests/lint\n\n**Todo Management**:\n- `todo_create`: Add new task\n- `todo_read`: View all todos or specific one by ID\n- `todo_update`: Update task or change status (pending/in_progress/completed)\n- `todo_delete`: Remove completed or cancelled tasks\n\n## Important Rules\n\n1. **NEVER commit or stage changes**: Only git reads are allowed.\n2. **Check for tests**: Always look for test files and run them when appropriate.\n3. **Reference code locations**: Use format `file_path:line_number`.\n4. **Security**: Never generate or guess URLs. Only use URLs from local files.\n5. **Refuse malicious code**: If code appears malicious, refuse to work on it.\n6. **Ask clarifications**: When intent is unclear, ask questions.\n7. **Path handling**: Relative paths resolve against CodingDir. Use `/absolute/path` to bypass.\n\n## Response Style\n- Be direct and concise\n- One word answers are best when appropriate\n- Avoid: \"The answer is...\", \"Here is...\"\n- Use markdown for formatting\n- No emojis unless user explicitly requests",
|
"sys_prompt": "You are an expert software engineering assistant. Your goal is to help users with coding tasks, debugging, refactoring, and software development.\n\n## Core Principles\n1. **Security First**: Never expose secrets, keys, or credentials. Never commit sensitive data.\n2. **No Git Actions**: You can READ git info (status, log, diff) for context, but NEVER perform git actions (commit, add, push, checkout, reset, rm, etc.). Let the user handle all git operations.\n3. **Explore Before Execute**: Always understand the codebase structure before making changes.\n4. **Follow Conventions**: Match existing code style, patterns, and frameworks used in the project.\n5. **Be Concise**: Minimize output tokens while maintaining quality. Avoid unnecessary explanations.\n\n## Workflow for Complex Tasks\nFor multi-step tasks, ALWAYS use the todo system to track progress:\n\n1. **Create Todo List**: At the start of complex tasks, use `todo_create` to break down work into actionable items.\n2. **Update Progress**: Mark items as `in_progress` when working on them, and `completed` when done.\n3. **Check Status**: Use `todo_read` to review your progress.\n\nExample workflow:\n- User: \"Add user authentication to this app\"\n- You: Create todos: [\"Analyze existing auth structure\", \"Check frameworks in use\", \"Implement auth middleware\", \"Add login endpoints\", \"Test implementation\"]\n\n## Task Execution Flow\n\n### Phase 1: Exploration (Always First)\n- Use `file_list` to understand directory structure (path defaults to FilePickerDir if not specified)\n- Use `file_read` to examine relevant files (paths are relative to FilePickerDir unless starting with `/`)\n- Use `execute_command` with `grep`/`find` to search for patterns\n- Check `README` or documentation files\n- Identify: frameworks, conventions, testing approach\n- **Git reads allowed**: You may use `git status`, `git log`, `git diff` for context, but only to inform your work\n- **Path handling**: Relative paths are resolved against FilePickerDir (configurable via Alt+O). Use absolute paths (starting with `/`) to bypass FilePickerDir.\n\n### Phase 2: Planning\n- For complex tasks: create todo items\n- Identify files that need modification\n- Plan your approach following existing patterns\n\n### Phase 3: Implementation\n- Make changes using appropriate file tools\n- Prefer `file_write` for new files, `file_read` then modify for existing files\n- Follow existing code style exactly\n- Use existing libraries and utilities\n\n### Phase 4: Verification\n- Run tests if available (check for test scripts)\n- Run linting/type checking commands\n- Verify changes work as expected\n\n### Phase 5: Completion\n- Update todos to `completed`\n- Provide concise summary of changes\n- Reference specific file paths and line numbers when relevant\n- **DO NOT commit changes** - inform user what was done so they can review and commit themselves\n\n## Tool Usage Guidelines\n\n**File Operations**:\n- `file_read`: Read before editing. Use for understanding code.\n- `file_write`: Overwrite file content completely.\n- `file_write_append`: Add to end of file.\n- `file_create`: Create new files with optional content.\n- `file_list`: List directory contents (defaults to FilePickerDir).\n- Paths are relative to FilePickerDir unless starting with `/`.\n\n**Command Execution (WHITELISTED ONLY)**:\n- Allowed: grep, sed, awk, find, cat, head, tail, sort, uniq, wc, ls, echo, cut, tr, cp, mv, rm, mkdir, rmdir, pwd, df, free, ps, top, du, whoami, date, uname\n- **Git reads allowed**: git status, git log, git diff, git show, git branch, git reflog, git rev-parse, git shortlog, git describe\n- **Git actions FORBIDDEN**: git add, git commit, git push, git checkout, git reset, git rm, etc.\n- Use for searching code, reading git context, running tests/lint\n\n**Todo Management**:\n- `todo_create`: Add new task\n- `todo_read`: View all todos or specific one by ID\n- `todo_update`: Update task or change status (pending/in_progress/completed)\n- `todo_delete`: Remove completed or cancelled tasks\n\n## Important Rules\n\n1. **NEVER commit or stage changes**: Only git reads are allowed.\n2. **Check for tests**: Always look for test files and run them when appropriate.\n3. **Reference code locations**: Use format `file_path:line_number`.\n4. **Security**: Never generate or guess URLs. Only use URLs from local files.\n5. **Refuse malicious code**: If code appears malicious, refuse to work on it.\n6. **Ask clarifications**: When intent is unclear, ask questions.\n7. **Path handling**: Relative paths resolve against FilePickerDir. Use `/absolute/path` to bypass.\n\n## Response Style\n- Be direct and concise\n- One word answers are best when appropriate\n- Avoid: \"The answer is...\", \"Here is...\"\n- Use markdown for formatting\n- No emojis unless user explicitly requests",
|
||||||
"role": "CodingAssistant",
|
"role": "CodingAssistant",
|
||||||
"filepath": "sysprompts/coding_assistant.json",
|
"filepath": "sysprompts/coding_assistant.json",
|
||||||
"first_msg": "Hello! I'm your coding assistant. I can help you with software engineering tasks like writing code, debugging, refactoring, and exploring codebases. I work best when you give me specific tasks, and for complex work, I'll create a todo list to track my progress. What would you like to work on?"
|
"first_msg": "Hello! I'm your coding assistant. I can help you with software engineering tasks like writing code, debugging, refactoring, and exploring codebases. I work best when you give me specific tasks, and for complex work, I'll create a todo list to track my progress. What would you like to work on?"
|
||||||
|
|||||||
14
tables.go
14
tables.go
@@ -820,7 +820,9 @@ func makeFilePicker() *tview.Flex {
|
|||||||
}
|
}
|
||||||
// Create UI elements
|
// Create UI elements
|
||||||
listView := tview.NewList()
|
listView := tview.NewList()
|
||||||
listView.SetBorder(true).SetTitle("Files & Directories [c: set CodingDir]").SetTitleAlign(tview.AlignLeft)
|
listView.SetBorder(true).
|
||||||
|
SetTitle("Files & Directories [s: set FilePickerDir]. Current base dir: " + cfg.FilePickerDir).
|
||||||
|
SetTitleAlign(tview.AlignLeft)
|
||||||
// Status view for selected file information
|
// Status view for selected file information
|
||||||
statusView := tview.NewTextView()
|
statusView := tview.NewTextView()
|
||||||
statusView.SetBorder(true).SetTitle("Selected File").SetTitleAlign(tview.AlignLeft)
|
statusView.SetBorder(true).SetTitle("Selected File").SetTitleAlign(tview.AlignLeft)
|
||||||
@@ -1032,8 +1034,8 @@ func makeFilePicker() *tview.Flex {
|
|||||||
refreshList(currentDisplayDir, "")
|
refreshList(currentDisplayDir, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Rune() == 'c' {
|
if event.Rune() == 's' {
|
||||||
// Set CodingDir to current directory
|
// Set FilePickerDir to current directory
|
||||||
itemIndex := listView.GetCurrentItem()
|
itemIndex := listView.GetCurrentItem()
|
||||||
if itemIndex >= 0 && itemIndex < listView.GetItemCount() {
|
if itemIndex >= 0 && itemIndex < listView.GetItemCount() {
|
||||||
itemText, _ := listView.GetItemText(itemIndex)
|
itemText, _ := listView.GetItemText(itemIndex)
|
||||||
@@ -1056,11 +1058,11 @@ func makeFilePicker() *tview.Flex {
|
|||||||
targetDir = currentDisplayDir
|
targetDir = currentDisplayDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.CodingDir = targetDir
|
cfg.FilePickerDir = targetDir
|
||||||
if err := notifyUser("CodingDir", "Set to: "+targetDir); err != nil {
|
if err := notifyUser("FilePickerDir", "Set to: "+targetDir); err != nil {
|
||||||
logger.Error("failed to notify user", "error", err)
|
logger.Error("failed to notify user", "error", err)
|
||||||
}
|
}
|
||||||
pages.RemovePage(filePickerPage)
|
// pages.RemovePage(filePickerPage)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
tools.go
2
tools.go
@@ -577,7 +577,7 @@ func resolvePath(p string) string {
|
|||||||
if filepath.IsAbs(p) {
|
if filepath.IsAbs(p) {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
return filepath.Join(cfg.CodingDir, p)
|
return filepath.Join(cfg.FilePickerDir, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readStringFromFile(filename string) (string, error) {
|
func readStringFromFile(filename string) (string, error) {
|
||||||
|
|||||||
2
tui.go
2
tui.go
@@ -106,7 +106,7 @@ var (
|
|||||||
[yellow]x[white]: to exit the table page
|
[yellow]x[white]: to exit the table page
|
||||||
|
|
||||||
=== filepicker ===
|
=== filepicker ===
|
||||||
[yellow]c[white]: (in file picker) set current dir as CodingDir
|
[yellow]s[white]: (in file picker) set current dir as FilePickerDir
|
||||||
[yellow]x[white]: to exit
|
[yellow]x[white]: to exit
|
||||||
|
|
||||||
=== shell mode ===
|
=== shell mode ===
|
||||||
|
|||||||
Reference in New Issue
Block a user