@@ -19,6 +19,9 @@ import (
1919 "github.com/GrayCodeAI/hawk/engine"
2020 "github.com/GrayCodeAI/hawk/plugin"
2121 "github.com/GrayCodeAI/hawk/session"
22+ "github.com/GrayCodeAI/hawk/shellmode"
23+ "github.com/GrayCodeAI/hawk/staleness"
24+ "github.com/GrayCodeAI/hawk/taste"
2225 "github.com/GrayCodeAI/hawk/tool"
2326)
2427
@@ -27,13 +30,13 @@ func slashCommands() []string {
2730 "/add" , "/add-dir" , "/agents" , "/agents-init" , "/audit" , "/branch" , "/branches" , "/bughunter" , "/clean" , "/clear" ,
2831 "/check" , "/color" , "/commit" , "/compact" , "/compress" , "/config" , "/context" , "/council" , "/design" ,
2932 "/copy" , "/cost" , "/cron" , "/diff" , "/doctor" , "/drop" , "/effort" , "/env" , "/exit" , "/explain" ,
30- "/export" , "/fast" , "/files" , "/focus" , "/fork" , "/help" , "/history" , "/hooks" , "/init" ,
33+ "/export" , "/fast" , "/feedback" , "/ files" , "/focus" , "/fork" , "/help" , "/history" , "/hooks" , "/init" ,
3134 "/integrity" , "/keybindings" , "/learn" , "/lint" , "/loop" , "/mcp" , "/memory" , "/metrics" , "/model" , "/new" ,
3235 "/hunt" , "/output-style" , "/permissions" , "/pin" , "/plan" , "/plugin" , "/plugins" ,
3336 "/power" , "/pr-comments" , "/provider-status" , "/quit" , "/refresh-model-catalog" , "/release-notes" ,
3437 "/reload-plugins" , "/remote-env" , "/rename" , "/render" , "/research" , "/resume" , "/retry" , "/review" , "/rewind" ,
35- "/run" , "/btw" , "/sandbox" , "/search" , "/security-review" , "/session" , "/share" , "/skills" , "/snapshot" , "/stats" ,
36- "/status" , "/statusline" , "/summary" , "/tag" , "/tasks" , "/test" , "/theme" ,
38+ "/run" , "/btw" , "/sandbox" , "/search" , "/security-review" , "/session" , "/share" , "/skills" , "/snapshot" , "/stale" , "/ stats" ,
39+ "/status" , "/statusline" , "/summary" , "/tag" , "/taste" , "/ tasks" , "/test" , "/theme" ,
3740 "/think" , "/think-back" , "/thinkback" , "/thinkback-play" , "/tokens" , "/tools" , "/undo" , "/upgrade" , "/usage" ,
3841 "/version" , "/vibe" , "/vim" , "/voice" , "/welcome" , "/yolo" ,
3942 }
@@ -75,6 +78,7 @@ var slashDescriptions = map[string]string{
7578 "/exit" : "Save and exit" ,
7679 "/explain" : "Trace code back to the commit that created it" ,
7780 "/export" : "Export session" ,
81+ "/feedback" : "Submit feedback about hawk" ,
7882 "/fast" : "Toggle fast mode" ,
7983 "/files" : "Show modified files" ,
8084 "/focus" : "Narrow agent attention to specific files/dirs" ,
@@ -106,6 +110,7 @@ var slashDescriptions = map[string]string{
106110 "/sandbox" : "Toggle sandbox mode" ,
107111 "/search" : "Search across sessions" ,
108112 "/snapshot" : "Manage file snapshots: list, restore <hash>, diff <hash>" ,
113+ "/stale" : "Show stale rules that may need updating or removal" ,
109114 "/security-review" : "Security audit" ,
110115 "/skills" : "List skills or manage: search, install, trending, info, remove, update, feedback, publish, audit" ,
111116 "/learn" : "LLM-powered skill advisor (/learn deep for source analysis)" ,
@@ -138,6 +143,7 @@ var slashDescriptions = map[string]string{
138143 "/share" : "Share session" ,
139144 "/statusline" : "Show status line info" ,
140145 "/tag" : "Tag current session" ,
146+ "/taste" : "Show learned taste preferences" ,
141147 "/theme" : "Change visual theme" ,
142148 "/think-back" : "Review reasoning decisions" ,
143149 "/thinkback" : "Review reasoning decisions" ,
@@ -428,6 +434,7 @@ func (m *chatModel) handleCommand(text string) (tea.Model, tea.Cmd) {
428434/env — Show provider environment status
429435/export — Export session to JSON
430436/fast — Toggle fast mode
437+ /feedback <msg> — Submit feedback (saved to ~/.hawk/feedback/)
431438/files — Show modified files
432439/help — This help message
433440/history — List saved sessions
@@ -457,10 +464,12 @@ func (m *chatModel) handleCommand(text string) (tea.Model, tea.Cmd) {
457464/share — Share session
458465/learn — LLM-powered skill advisor (deep, update)
459466/skills — List, search, install, remove skills
467+ /stale — Show stale rules that may need removal
460468/stats — Session statistics
461469/status — Session status
462470/summary — Summarize the current session
463471/tag <label> — Tag session
472+ /taste — Show learned coding style preferences
464473/tasks — Show task list
465474/teams — Show team info
466475/theme <t> — Set theme (dark/light/auto)
@@ -1472,6 +1481,58 @@ func (m *chatModel) handleCommand(text string) (tea.Model, tea.Cmd) {
14721481 m .messages = append (m .messages , displayMsg {role : "system" , content : fmt .Sprintf ("Exported to: %s" , exportPath )})
14731482 }
14741483 return m , nil
1484+ case "/feedback" :
1485+ body := strings .TrimSpace (strings .TrimPrefix (text , "/feedback" ))
1486+ if body == "" {
1487+ m .messages = append (m .messages , displayMsg {role : "system" , content : "Usage: /feedback <message>\n Captures session context and saves feedback to ~/.hawk/feedback/" })
1488+ return m , nil
1489+ }
1490+ home , _ := os .UserHomeDir ()
1491+ feedDir := filepath .Join (home , ".hawk" , "feedback" )
1492+ os .MkdirAll (feedDir , 0755 )
1493+ report := fmt .Sprintf (`{"timestamp":%q,"version":%q,"model":%q,"provider":%q,"category":"session","body":%q,"session_id":%q}` ,
1494+ time .Now ().Format (time .RFC3339 ), version , m .session .Model (), m .session .Provider (), body , m .sessionID )
1495+ fname := fmt .Sprintf ("feedback-%s.json" , time .Now ().Format ("20060102-150405" ))
1496+ fpath := filepath .Join (feedDir , fname )
1497+ if err := os .WriteFile (fpath , []byte (report ), 0644 ); err != nil {
1498+ m .messages = append (m .messages , displayMsg {role : "error" , content : "Failed to save feedback: " + err .Error ()})
1499+ } else {
1500+ m .messages = append (m .messages , displayMsg {role : "system" , content : fmt .Sprintf ("Feedback saved to %s" , fpath )})
1501+ }
1502+ return m , nil
1503+ case "/stale" :
1504+ if m .stalenessDetector == nil {
1505+ m .messages = append (m .messages , displayMsg {role : "system" , content : "No staleness data available yet. Rules will be tracked as they are used." })
1506+ return m , nil
1507+ }
1508+ threshold := 7 * 24 * time .Hour // 7 days default
1509+ if len (parts ) > 1 {
1510+ if d , err := time .ParseDuration (parts [1 ]); err == nil {
1511+ threshold = d
1512+ }
1513+ }
1514+ staleRules := m .stalenessDetector .CheckStaleness (threshold )
1515+ if len (staleRules ) == 0 {
1516+ m .messages = append (m .messages , displayMsg {role : "system" , content : "No stale rules detected. All rules have been used within the threshold." })
1517+ } else {
1518+ m .messages = append (m .messages , displayMsg {role : "system" , content : stalenessFormatReport (staleRules )})
1519+ }
1520+ return m , nil
1521+ case "/taste" :
1522+ store , err := tasteStoreForSession ()
1523+ if err != nil {
1524+ m .messages = append (m .messages , displayMsg {role : "error" , content : "Taste store error: " + err .Error ()})
1525+ return m , nil
1526+ }
1527+ cwd , _ := os .Getwd ()
1528+ projectID := filepath .Base (cwd )
1529+ profile , err := store .Load (projectID )
1530+ if err != nil {
1531+ m .messages = append (m .messages , displayMsg {role : "error" , content : "Load taste profile: " + err .Error ()})
1532+ return m , nil
1533+ }
1534+ m .messages = append (m .messages , displayMsg {role : "system" , content : profile .Summary ()})
1535+ return m , nil
14751536 case "/rename" :
14761537 if len (parts ) < 2 {
14771538 m .messages = append (m .messages , displayMsg {role : "system" , content : "Usage: /rename <new-session-name>" })
@@ -1985,26 +2046,33 @@ func (m *chatModel) handleShellEscape(command string) (tea.Model, tea.Cmd) {
19852046 if command == "" {
19862047 return m , nil
19872048 }
2049+
2050+ // Warn about destructive commands.
2051+ if shellmode .IsDestructive (command ) {
2052+ m .messages = append (m .messages , displayMsg {role : "error" , content : "Warning: potentially destructive command detected. Use with caution." })
2053+ }
2054+
19882055 m .messages = append (m .messages , displayMsg {role : "system" , content : "$ " + command })
19892056 m .viewDirty = true
19902057
1991- cmd := exec .Command ("sh" , "-c" , command )
1992- out , err := cmd .CombinedOutput ()
1993- result := strings .TrimRight (string (out ), "\n " )
1994- if err != nil && result == "" {
1995- result = err .Error ()
1996- }
1997- if result != "" {
1998- // Truncate very long output
1999- if len (result ) > 4000 {
2000- lines := strings .Split (result , "\n " )
2058+ result := shellmode .ExecuteShell (context .Background (), command )
2059+ output := result .Stdout + result .Stderr
2060+ output = strings .TrimRight (output , "\n " )
2061+
2062+ if output != "" {
2063+ // Truncate very long output.
2064+ if len (output ) > 4000 {
2065+ lines := strings .Split (output , "\n " )
20012066 if len (lines ) > 40 {
20022067 head := strings .Join (lines [:20 ], "\n " )
20032068 tail := strings .Join (lines [len (lines )- 20 :], "\n " )
2004- result = head + fmt .Sprintf ("\n \n ... (%d lines omitted) ...\n \n " , len (lines )- 40 ) + tail
2069+ output = head + fmt .Sprintf ("\n \n ... (%d lines omitted) ...\n \n " , len (lines )- 40 ) + tail
20052070 }
20062071 }
2007- m .messages = append (m .messages , displayMsg {role : "tool_result" , content : result })
2072+ m .messages = append (m .messages , displayMsg {role : "tool_result" , content : output })
2073+ }
2074+ if result .ExitCode != 0 && output == "" {
2075+ m .messages = append (m .messages , displayMsg {role : "error" , content : fmt .Sprintf ("exit code: %d" , result .ExitCode )})
20082076 }
20092077 m .viewDirty = true
20102078 return m , nil
@@ -2017,3 +2085,13 @@ func truncate(s string, max int) string {
20172085 }
20182086 return s [:max ] + "…"
20192087}
2088+
2089+ // tasteStoreForSession returns a taste store using the default location.
2090+ func tasteStoreForSession () (* taste.Store , error ) {
2091+ return taste .NewStore ("" )
2092+ }
2093+
2094+ // stalenessFormatReport formats stale rules for display.
2095+ func stalenessFormatReport (rules []staleness.StaleRule ) string {
2096+ return staleness .FormatReport (rules )
2097+ }
0 commit comments