Skip to content

Commit bf7e777

Browse files
committed
feat: dino and eyes
1 parent 1b43e7c commit bf7e777

22 files changed

Lines changed: 1084 additions & 306 deletions

.claude/settings.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(go build:*)",
5+
"Bash(./docsgpt-cli --version)",
6+
"Bash(./docsgpt-cli --help)",
7+
"Bash(./docsgpt-cli config:*)",
8+
"Bash(./docsgpt-cli chat:*)",
9+
"Bash(./docsgpt-cli keys:*)",
10+
"Bash(curl -s -X POST http://localhost:7091/v1/chat/completions -H 'Authorization: Bearer 46ded8a6-b308-421d-8ed3-839175fe9ad3' -H 'Content-Type: application/json' -d '{:*)",
11+
"Bash(curl -s -N -X POST http://localhost:7091/v1/chat/completions -H 'Authorization: Bearer 46ded8a6-b308-421d-8ed3-839175fe9ad3' -H 'Content-Type: application/json' -d '{:*)",
12+
"Bash(timeout 30 ./docsgpt-cli ask --no-stream \"Create a file called TESTFILE123.md with the text 'hello from docsgpt'\")",
13+
"Bash(./docsgpt-cli ask:*)",
14+
"Bash(grep -v grep echo \"---\")"
15+
]
16+
}
17+
}

cmd/ask.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78
"time"
89

@@ -12,7 +13,6 @@ import (
1213
"docsgpt-cli/internal/display"
1314
"docsgpt-cli/internal/tools"
1415

15-
"github.com/fatih/color"
1616
"github.com/spf13/cobra"
1717
)
1818

@@ -51,39 +51,41 @@ This command will provide a contextual answer and, if applicable, copy a relevan
5151
{Role: "user", Content: fullQuestion},
5252
}
5353

54-
green := color.New(color.FgGreen).SprintFunc()
55-
fmt.Printf(green("Key: %s\n"), keyName)
56-
fmt.Printf(green(" ❯ "))
54+
cwd, _ := os.Getwd()
55+
fmt.Println(display.RenderHeader(keyName, baseURL, cwd))
56+
fmt.Print(display.Prompt("❯ "))
5757

5858
ctx := context.Background()
59-
toolDefs := tools.ToolDefinitions()
59+
var toolDefs []api.Tool
60+
if !globalNoContext {
61+
toolDefs = tools.ToolDefinitions()
62+
}
6063
timeout := time.Duration(globalTimeout) * time.Second
6164

65+
renderer := display.NewStreamRenderer()
66+
6267
onDelta := func(delta api.Delta, finishReason string) {
63-
display.StreamDelta(delta)
68+
renderer.Delta(delta)
6469
}
6570

6671
onToolCall := func(tc api.ToolCall) string {
6772
return handleToolCall(tc, timeout)
6873
}
6974

70-
updatedHistory, err := client.RunWithTools(
75+
_, err = client.RunWithTools(
7176
ctx, messages, toolDefs, !globalNoStream, onDelta, onToolCall,
7277
)
7378
if err != nil {
7479
return err
7580
}
7681
fmt.Println()
7782

78-
// Find the last assistant message for clipboard
79-
var answer string
80-
for i := len(updatedHistory) - 1; i >= 0; i-- {
81-
if updatedHistory[i].Role == "assistant" {
82-
answer = updatedHistory[i].Content
83-
break
84-
}
83+
// Render markdown for the final answer if it contains formatting
84+
if rendered := renderer.Finish(); rendered != "" {
85+
fmt.Print(rendered)
8586
}
8687

88+
answer := renderer.Content()
8789
command := extractCommand(answer)
8890
if command != "" {
8991
copyToClipboard(command)

cmd/chat.go

Lines changed: 129 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package cmd
22

33
import (
4-
"bufio"
54
"context"
65
"fmt"
76
"os"
8-
"os/signal"
97
"strings"
108
"time"
119

@@ -15,7 +13,7 @@ import (
1513
"docsgpt-cli/internal/display"
1614
"docsgpt-cli/internal/tools"
1715

18-
"github.com/fatih/color"
16+
prompt "github.com/c-bata/go-prompt"
1917
"github.com/spf13/cobra"
2018
)
2119

@@ -27,7 +25,10 @@ var chatCmd = &cobra.Command{
2725
Special commands:
2826
/quit - Exit the chat session
2927
/clear - Clear conversation history
30-
/copy - Copy the last code block to clipboard`,
28+
/copy - Copy the last code block to clipboard
29+
/think - Toggle reasoning visibility
30+
31+
Type "/" to see available commands with live autocomplete.`,
3132
RunE: func(cmd *cobra.Command, args []string) error {
3233
cfg, err := config.Load()
3334
if err != nil {
@@ -42,10 +43,11 @@ Special commands:
4243
baseURL := cfg.ResolveURL(globalURL)
4344
client := api.NewClient(baseURL, apiKey)
4445

45-
green := color.New(color.FgGreen).SprintFunc()
46-
fmt.Printf(green("Connected with key: %s\n"), keyName)
47-
fmt.Printf(green("Server: %s\n"), baseURL)
48-
fmt.Println("Type /quit to exit, /clear to reset history, /copy to copy last code block.")
46+
cwd, _ := os.Getwd()
47+
fmt.Println(display.RenderHeader(keyName, baseURL, cwd))
48+
if hints := display.RenderHints("chat"); hints != "" {
49+
fmt.Println(hints)
50+
}
4951
fmt.Println()
5052

5153
var history []api.Message
@@ -65,96 +67,138 @@ Special commands:
6567
},
6668
}
6769

68-
func runChatLoop(client *api.Client, history []api.Message) error {
69-
reader := bufio.NewReader(os.Stdin)
70-
var lastAnswer string
71-
72-
// Handle Ctrl+C gracefully
73-
sigCh := make(chan os.Signal, 1)
74-
signal.Notify(sigCh, os.Interrupt)
75-
go func() {
76-
<-sigCh
77-
fmt.Println("\nGoodbye!")
70+
// chatSession holds the mutable state for an interactive chat.
71+
type chatSession struct {
72+
client *api.Client
73+
history []api.Message
74+
lastAnswer string
75+
showReasoning bool
76+
toolDefs []api.Tool
77+
timeout time.Duration
78+
}
79+
80+
func (s *chatSession) executor(input string) {
81+
input = strings.TrimSpace(input)
82+
if input == "" {
83+
return
84+
}
85+
86+
switch input {
87+
case "/quit":
88+
fmt.Println("Goodbye!")
7889
os.Exit(0)
79-
}()
90+
case "/clear":
91+
var newHistory []api.Message
92+
if len(s.history) > 0 && s.history[0].Role == "system" {
93+
newHistory = append(newHistory, s.history[0])
94+
}
95+
s.history = newHistory
96+
s.lastAnswer = ""
97+
fmt.Println("History cleared.")
98+
return
99+
case "/copy":
100+
if s.lastAnswer == "" {
101+
printError("No previous response to copy from.")
102+
return
103+
}
104+
command := extractCommand(s.lastAnswer)
105+
if command != "" {
106+
copyToClipboard(command)
107+
} else {
108+
printError("No code block found in last response.")
109+
}
110+
return
111+
case "/think":
112+
s.showReasoning = !s.showReasoning
113+
if s.showReasoning {
114+
fmt.Println(display.Muted("Reasoning: visible"))
115+
} else {
116+
fmt.Println(display.Muted("Reasoning: hidden"))
117+
}
118+
return
119+
}
80120

81-
toolDefs := tools.ToolDefinitions()
82-
timeout := time.Duration(globalTimeout) * time.Second
121+
s.history = append(s.history, api.Message{Role: "user", Content: input})
122+
ctx := context.Background()
83123

84-
for {
85-
green := color.New(color.FgGreen).SprintFunc()
86-
fmt.Print(green("> "))
124+
renderer := display.NewStreamRenderer()
125+
renderer.ShowReasoning = s.showReasoning
87126

88-
input, err := reader.ReadString('\n')
89-
if err != nil {
90-
return nil // EOF
91-
}
92-
input = strings.TrimSpace(input)
127+
onDelta := func(delta api.Delta, finishReason string) {
128+
renderer.Delta(delta)
129+
}
93130

94-
if input == "" {
95-
continue
96-
}
131+
onToolCall := func(tc api.ToolCall) string {
132+
return handleToolCall(tc, s.timeout)
133+
}
97134

98-
switch input {
99-
case "/quit":
100-
fmt.Println("Goodbye!")
101-
return nil
102-
case "/clear":
103-
// Keep system message if present
104-
var newHistory []api.Message
105-
if len(history) > 0 && history[0].Role == "system" {
106-
newHistory = append(newHistory, history[0])
107-
}
108-
history = newHistory
109-
lastAnswer = ""
110-
fmt.Println("History cleared.")
111-
continue
112-
case "/copy":
113-
if lastAnswer == "" {
114-
printError("No previous response to copy from.")
115-
continue
116-
}
117-
command := extractCommand(lastAnswer)
118-
if command != "" {
119-
copyToClipboard(command)
120-
} else {
121-
printError("No code block found in last response.")
122-
}
123-
continue
124-
}
135+
updatedHistory, err := s.client.RunWithTools(
136+
ctx, s.history, s.toolDefs, !globalNoStream, onDelta, onToolCall,
137+
)
138+
if err != nil {
139+
printError(err.Error())
140+
return
141+
}
142+
fmt.Println()
125143

126-
history = append(history, api.Message{Role: "user", Content: input})
127-
ctx := context.Background()
144+
if rendered := renderer.Finish(); rendered != "" {
145+
fmt.Print(rendered)
146+
}
128147

129-
onDelta := func(delta api.Delta, finishReason string) {
130-
display.StreamDelta(delta)
131-
}
148+
s.history = updatedHistory
149+
s.lastAnswer = renderer.Content()
132150

133-
onToolCall := func(tc api.ToolCall) string {
134-
return handleToolCall(tc, timeout)
135-
}
151+
fmt.Println()
152+
}
136153

137-
updatedHistory, err := client.RunWithTools(
138-
ctx, history, toolDefs, !globalNoStream, onDelta, onToolCall,
139-
)
140-
if err != nil {
141-
printError(err.Error())
142-
continue
143-
}
144-
fmt.Println()
154+
func (s *chatSession) completer(d prompt.Document) []prompt.Suggest {
155+
text := d.TextBeforeCursor()
156+
if !strings.HasPrefix(text, "/") {
157+
return nil
158+
}
145159

146-
history = updatedHistory
160+
suggestions := []prompt.Suggest{
161+
{Text: "/quit", Description: "Exit the chat session"},
162+
{Text: "/clear", Description: "Clear conversation history"},
163+
{Text: "/copy", Description: "Copy last code block to clipboard"},
164+
{Text: "/think", Description: "Toggle reasoning visibility"},
165+
}
147166

148-
// Find the last assistant message for lastAnswer
149-
for i := len(history) - 1; i >= 0; i-- {
150-
if history[i].Role == "assistant" {
151-
lastAnswer = history[i].Content
152-
break
153-
}
154-
}
167+
return prompt.FilterHasPrefix(suggestions, text, true)
168+
}
155169

156-
fmt.Println()
170+
func runChatLoop(client *api.Client, history []api.Message) error {
171+
var toolDefs []api.Tool
172+
if !globalNoContext {
173+
toolDefs = tools.ToolDefinitions()
174+
}
175+
176+
session := &chatSession{
177+
client: client,
178+
history: history,
179+
toolDefs: toolDefs,
180+
timeout: time.Duration(globalTimeout) * time.Second,
157181
}
182+
183+
p := prompt.New(
184+
session.executor,
185+
session.completer,
186+
prompt.OptionPrefix("> "),
187+
prompt.OptionPrefixTextColor(prompt.Purple),
188+
prompt.OptionSuggestionBGColor(prompt.DarkGray),
189+
prompt.OptionSuggestionTextColor(prompt.White),
190+
prompt.OptionSelectedSuggestionBGColor(prompt.Purple),
191+
prompt.OptionSelectedSuggestionTextColor(prompt.White),
192+
prompt.OptionDescriptionBGColor(prompt.DarkGray),
193+
prompt.OptionDescriptionTextColor(prompt.White),
194+
prompt.OptionSelectedDescriptionBGColor(prompt.Purple),
195+
prompt.OptionSelectedDescriptionTextColor(prompt.White),
196+
prompt.OptionScrollbarBGColor(prompt.DarkGray),
197+
prompt.OptionScrollbarThumbColor(prompt.Purple),
198+
prompt.OptionShowCompletionAtStart(),
199+
)
200+
p.Run()
201+
return nil
158202
}
159203

160204
func handleToolCall(tc api.ToolCall, timeout time.Duration) string {
@@ -164,8 +208,7 @@ func handleToolCall(tc api.ToolCall, timeout time.Duration) string {
164208
if normalizedName == "run_command" {
165209
safe, reason := tools.IsSafe(tc.Function.Arguments)
166210
if !safe {
167-
red := color.New(color.FgRed).SprintFunc()
168-
fmt.Printf("\n%s Command blocked: %s\n", red("✗"), reason)
211+
fmt.Printf("\n%s Command blocked: %s\n", display.Danger("✗"), reason)
169212
return fmt.Sprintf("Command was blocked for safety: %s", reason)
170213
}
171214
}

0 commit comments

Comments
 (0)