@@ -3,7 +3,10 @@ package main
33import (
44 "context"
55 "fmt"
6+ "os"
7+ "path/filepath"
68 "runtime"
9+ "sort"
710 "strings"
811
912 "github.com/charmbracelet/bubbles/textarea"
@@ -15,14 +18,19 @@ import (
1518)
1619
1720type model struct {
18- viewport viewport.Model
19- messages []string
20- textarea textarea.Model
21- err error
22- brain * brain.Brain
23- width int
24- height int
25- initialized bool
21+ viewport viewport.Model
22+ messages []string
23+ textarea textarea.Model
24+ err error
25+ brain * brain.Brain
26+ width int
27+ height int
28+ initialized bool
29+ showTree bool
30+ treeView string
31+ suggestions []string
32+ suggestionIdx int
33+ triggerChar string // '/' or '#'
2634}
2735
2836var (
@@ -51,13 +59,36 @@ var (
5159
5260 helpStyle = lipgloss .NewStyle ().
5361 Foreground (lipgloss .Color ("#626262" ))
62+
63+ tagStyle = lipgloss .NewStyle ().
64+ Foreground (lipgloss .Color ("#FFD700" )).
65+ Bold (true ).
66+ Italic (true )
67+
68+ suggestionStyle = lipgloss .NewStyle ().
69+ Foreground (lipgloss .Color ("#7D56F4" )).
70+ Background (lipgloss .Color ("#222222" ))
71+
72+ selectedSuggestionStyle = lipgloss .NewStyle ().
73+ Foreground (lipgloss .Color ("#FAFAFA" )).
74+ Background (lipgloss .Color ("#7D56F4" )).
75+ Bold (true )
76+
77+ treeStyle = lipgloss .NewStyle ().
78+ Border (lipgloss .NormalBorder (), false , false , false , true ).
79+ BorderForeground (lipgloss .Color ("#444444" )).
80+ PaddingLeft (2 )
5481)
5582
5683type chatState struct {
5784 Messages []string `json:"messages"`
5885 Input string `json:"input"`
5986}
6087
88+ var allCommands = []string {
89+ "/help" , "/status" , "/cwd" , "/version" , "/clear" , "/exit" , "/show-tree" ,
90+ }
91+
6192func initialModel (b * brain.Brain ) * model {
6293 ta := textarea .New ()
6394 ta .Placeholder = "Send a message or type / for commands..."
@@ -128,9 +159,28 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128159 m .width = msg .Width
129160 m .height = msg .Height
130161 m .viewport .Width = msg .Width
131- m .textarea .SetWidth (msg .Width )
162+ if m .showTree {
163+ m .viewport .Width = msg .Width / 2
164+ }
165+ m .textarea .SetWidth (m .viewport .Width )
132166 m .viewport .Height = msg .Height - m .textarea .Height () - 6
133167 case tea.KeyMsg :
168+ // Suggestion navigation
169+ if len (m .suggestions ) > 0 {
170+ switch msg .String () {
171+ case "tab" , "down" :
172+ m .suggestionIdx = (m .suggestionIdx + 1 ) % len (m .suggestions )
173+ return m , nil
174+ case "shift+tab" , "up" :
175+ m .suggestionIdx = (m .suggestionIdx - 1 + len (m .suggestions )) % len (m .suggestions )
176+ return m , nil
177+ case "enter" :
178+ // Accept suggestion
179+ m .applySuggestion ()
180+ return m , nil
181+ }
182+ }
183+
134184 switch msg .String () {
135185 case "ctrl+c" , "esc" :
136186 m .saveState ()
@@ -149,14 +199,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
149199 // Add user message via a response update
150200 m .messages = append (m .messages , userStyle .Render ("You: " )+ v )
151201 m .textarea .Reset ()
202+ m .updateSuggestions ("" ) // Clear suggestions
152203 m .viewport .SetContent (strings .Join (m .messages , "\n \n " ))
153204 m .viewport .GotoBottom ()
154205
155206 m .saveState ()
156207
157208 // Process via brain
158209 return m , m .processRequest (v )
159-
210+ default :
211+ // After normal keypress, update suggestions
212+ m .updateSuggestions (m .textarea .Value ())
213+ m .updateDynamicPreview ()
160214 }
161215 case brain.Response :
162216 if msg .Error != nil {
@@ -172,6 +226,160 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
172226 return m , tea .Batch (tiCmd , vpCmd )
173227}
174228
229+ func (m * model ) updateSuggestions (val string ) {
230+ m .suggestions = nil
231+ m .suggestionIdx = 0
232+ m .triggerChar = ""
233+
234+ if val == "" {
235+ return
236+ }
237+
238+ // Find the word being typed
239+ words := strings .Fields (val )
240+ if len (words ) == 0 {
241+ if strings .HasSuffix (val , "/" ) {
242+ m .triggerChar = "/"
243+ m .suggestions = append ([]string {}, allCommands ... )
244+ sort .Strings (m .suggestions )
245+ } else if strings .HasSuffix (val , "#" ) {
246+ m .triggerChar = "#"
247+ m .suggestions = m .getFileSuggestions ("" )
248+ }
249+ return
250+ }
251+
252+ lastWord := words [len (words )- 1 ]
253+ if strings .HasPrefix (lastWord , "/" ) {
254+ m .triggerChar = "/"
255+ for _ , cmd := range allCommands {
256+ if strings .HasPrefix (cmd , lastWord ) {
257+ m .suggestions = append (m .suggestions , cmd )
258+ }
259+ }
260+ sort .Strings (m .suggestions )
261+ } else if strings .HasPrefix (lastWord , "#" ) {
262+ m .triggerChar = "#"
263+ m .suggestions = m .getFileSuggestions (lastWord [1 :])
264+ }
265+ }
266+
267+ func (m * model ) getFileSuggestions (prefix string ) []string {
268+ var suggestions []string
269+ root , _ := os .Getwd ()
270+
271+ filepath .WalkDir (root , func (path string , d os.DirEntry , err error ) error {
272+ if err != nil || len (suggestions ) > 30 {
273+ return nil
274+ }
275+
276+ name := d .Name ()
277+ if d .IsDir () {
278+ if name == ".git" || name == "node_modules" || name == "vendor" || name == "bin" || name == "dist" {
279+ return filepath .SkipDir
280+ }
281+ if prefix != "" && ! strings .HasPrefix (name , prefix ) && ! strings .HasPrefix (path , prefix ) {
282+ return nil
283+ }
284+ }
285+
286+ rel , _ := filepath .Rel (root , path )
287+ if rel == "." {
288+ return nil
289+ }
290+
291+ if prefix == "" || strings .HasPrefix (rel , prefix ) || strings .HasPrefix (name , prefix ) {
292+ suggestions = append (suggestions , rel )
293+ }
294+
295+ return nil
296+ })
297+
298+ sort .Strings (suggestions )
299+ return suggestions
300+ }
301+
302+ func (m * model ) applySuggestion () {
303+ if len (m .suggestions ) == 0 {
304+ return
305+ }
306+
307+ val := m .textarea .Value ()
308+ words := strings .Fields (val )
309+ if len (words ) == 0 {
310+ m .textarea .SetValue (m .suggestions [m .suggestionIdx ] + " " )
311+ } else {
312+ words [len (words )- 1 ] = m .triggerChar + m .suggestions [m .suggestionIdx ]
313+ m .textarea .SetValue (strings .Join (words , " " ) + " " )
314+ }
315+ m .textarea .SetCursor (len (m .textarea .Value ()))
316+ m .suggestions = nil
317+ m .updateDynamicPreview ()
318+ }
319+
320+ func (m * model ) updateDynamicPreview () {
321+ if ! m .showTree {
322+ return
323+ }
324+
325+ val := m .textarea .Value ()
326+ tags := m .extractTags (val )
327+
328+ if len (tags ) > 0 {
329+ // Show the last tag's content
330+ lastTag := tags [len (tags )- 1 ]
331+ content , err := os .ReadFile (lastTag )
332+ if err == nil {
333+ m .treeView = string (content )
334+ return
335+ }
336+
337+ // If it's a directory, show tree
338+ info , err := os .Stat (lastTag )
339+ if err == nil && info .IsDir () {
340+ m .treeView = m .renderTree (lastTag )
341+ return
342+ }
343+ }
344+
345+ // Default to workspace tree
346+ m .treeView = m .renderTree ("." )
347+ }
348+
349+ func (m * model ) extractTags (val string ) []string {
350+ var tags []string
351+ words := strings .Fields (val )
352+ for _ , w := range words {
353+ if strings .HasPrefix (w , "#" ) {
354+ path := strings .TrimPrefix (w , "#" )
355+ if _ , err := os .Stat (path ); err == nil {
356+ tags = append (tags , path )
357+ }
358+ }
359+ }
360+ return tags
361+ }
362+
363+ func (m * model ) renderTree (root string ) string {
364+ var sb strings.Builder
365+ sb .WriteString (systemStyle .Render (" EXPLORER: " + root ) + "\n \n " )
366+
367+ entries , _ := os .ReadDir (root )
368+ for _ , entry := range entries {
369+ name := entry .Name ()
370+ if strings .HasPrefix (name , "." ) && name != ".env" {
371+ continue
372+ }
373+
374+ icon := "📄 "
375+ if entry .IsDir () {
376+ icon = "📁 "
377+ }
378+ sb .WriteString (icon + name + "\n " )
379+ }
380+ return sb .String ()
381+ }
382+
175383func (m * model ) processRequest (content string ) tea.Cmd {
176384 return func () tea.Msg {
177385 ctx := context .Background ()
@@ -200,6 +408,16 @@ func (m *model) handleSlashCommand(cmd string) (tea.Model, tea.Cmd) {
200408 m .messages = append (m .messages , systemStyle .Render (" CWD " ) + " " + helpStyle .Render (snapshot .WorkingDir ))
201409 case "/version" :
202410 m .messages = append (m .messages , systemStyle .Render (" VERSION " ) + "\n " + helpStyle .Render (fmt .Sprintf ("App: %s\n Commit: %s\n Compiler: %s" , Version , Commit , runtime .Version ())))
411+ case "/show-tree" :
412+ m .showTree = ! m .showTree
413+ if m .showTree {
414+ m .viewport .Width = m .width / 2
415+ m .treeView = m .renderTree ("." )
416+ } else {
417+ m .viewport .Width = m .width
418+ }
419+ m .textarea .SetWidth (m .viewport .Width )
420+ m .messages = append (m .messages , systemStyle .Render (" Sideview Toggled " ))
203421 case "/clear" :
204422 m .messages = []string {}
205423 m .viewport .SetContent (systemStyle .Render (" Session Cleared " ))
@@ -216,19 +434,66 @@ func (m *model) handleSlashCommand(cmd string) (tea.Model, tea.Cmd) {
216434}
217435
218436func (m * model ) View () string {
219- header := titleStyle .Render (" vibeauracle " ) + " " + helpStyle .Render ("v" + Version )
437+ header := titleStyle .Render (" vibeauracle " ) + " " + helpStyle .Render ("v" + Version )
220438 border := strings .Repeat ("─" , m .width )
221439 if m .width > 20 {
222440 border = strings .Repeat ("─" , m .width - 1 )
223441 }
224442
225- return fmt .Sprintf (
443+ mainContent := m .viewport .View ()
444+ if m .showTree {
445+ mainContent = lipgloss .JoinHorizontal (lipgloss .Top ,
446+ m .viewport .View (),
447+ treeStyle .Render (m .treeView ),
448+ )
449+ }
450+
451+ suggestionView := ""
452+ if len (m .suggestions ) > 0 {
453+ var sbs []string
454+ for i , s := range m .suggestions {
455+ style := suggestionStyle
456+ if i == m .suggestionIdx {
457+ style = selectedSuggestionStyle
458+ }
459+
460+ // Format: name path (truncated)
461+ name := filepath .Base (s )
462+ if m .triggerChar == "/" {
463+ name = s
464+ }
465+ dir := filepath .Dir (s )
466+ if dir == "." {
467+ dir = ""
468+ } else {
469+ dir = " " + dir
470+ if len (dir ) > 20 {
471+ dir = dir [:17 ] + "..."
472+ }
473+ }
474+
475+ sbs = append (sbs , style .Render (fmt .Sprintf (" %-15s %s " , name , dir )))
476+ }
477+ suggestionView = lipgloss .NewStyle ().
478+ Border (lipgloss .RoundedBorder ()).
479+ BorderForeground (lipgloss .Color ("#7D56F4" )).
480+ Render (strings .Join (sbs , "\n " ))
481+ }
482+
483+ view := fmt .Sprintf (
226484 "%s\n %s\n %s\n %s\n %s" ,
227485 header ,
228486 lipgloss .NewStyle ().Foreground (lipgloss .Color ("#444444" )).Render (border ),
229- m . viewport . View () ,
487+ mainContent ,
230488 lipgloss .NewStyle ().Foreground (lipgloss .Color ("#444444" )).Render (border ),
231489 m .textarea .View (),
232- ) + "\n "
490+ )
491+
492+ if suggestionView != "" {
493+ // Overlay logic: simplified for TUI
494+ view += "\n " + suggestionView
495+ }
496+
497+ return view + "\n "
233498}
234499
0 commit comments