diff --git a/README.md b/README.md
index 4a430b2..4afcba3 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,19 @@ To update `hishtory` to the latest version, just run `hishtory update` to secure
### Advanced Features
+
+AI Shell Assistance
+If you are ever trying to figure out a bash command and searching your history isn't working, you can query ChatGPT by prefixing your query with `?`. For example, press `Control+R` and then type in `? list all files larger than 1MB`:
+
+
+
+If you would like to:
+* Disable this, you can run `hishtory config-set ai-completion false`
+* Run this with your own OpenAI API key (thereby ensuring that your queries do not pass through the centrally hosted hiSHtory server), you can run `export OPENAI_API_KEY='...'`
+
+
+
+
TUI key bindings
The TUI (opened via `Control+R`) supports a number of key bindings:
@@ -74,6 +87,8 @@ The TUI (opened via `Control+R`) supports a number of key bindings:
| Shift + Left/Right | Scroll the table left/right |
| Control+K | Delete the selected command |
+Press `Control+H` to view a help page documenting these.
+
diff --git a/backend/web/landing/www/img/aidemo.png b/backend/web/landing/www/img/aidemo.png
new file mode 100644
index 0000000..5c18a8b
Binary files /dev/null and b/backend/web/landing/www/img/aidemo.png differ
diff --git a/client/client_test.go b/client/client_test.go
index 21c92b7..ec6c935 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -1934,7 +1934,7 @@ func testTui_ai(t *testing.T) {
require.NoError(t, err)
// Test running an AI query
- tester.RunInteractiveShell(t, `hishtory config-set beta-mode true`)
+ require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get ai-completion`)))
out := captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
{Keys: "hishtory SPACE tquery ENTER"},
// ExtraDelay since AI queries are debounced and thus slower
@@ -1942,6 +1942,16 @@ func testTui_ai(t *testing.T) {
})
out = stripTuiCommandPrefix(t, out)
testutils.CompareGoldens(t, out, "TestTui-AiQuery")
+
+ // Test that when it is disabled, no AI queries are run
+ tester.RunInteractiveShell(t, `hishtory config-set ai-completion false`)
+ out = captureTerminalOutput(t, tester, []string{
+ "hishtory SPACE tquery ENTER",
+ "'?myQuery'",
+ })
+ out = stripTuiCommandPrefix(t, out)
+ testutils.CompareGoldens(t, out, "TestTui-AiQuery-Disabled")
+
}
func testControlR(t *testing.T, tester shellTester, shellName string, onlineStatus OnlineStatus) {
diff --git a/client/cmd/configGet.go b/client/cmd/configGet.go
index af47030..c3d47ae 100644
--- a/client/cmd/configGet.go
+++ b/client/cmd/configGet.go
@@ -50,6 +50,17 @@ var getFilterDuplicateCommandsCmd = &cobra.Command{
},
}
+var getEnableAiCompletion = &cobra.Command{
+ Use: "ai-completion",
+ Short: "Enable AI completion for searches starting with '?'",
+ Long: "Note that AI completion requests are sent to the shared hiSHtory backend and then to OpenAI. Requests are not logged, but still be careful not to put anything sensitive in queries.",
+ Run: func(cmd *cobra.Command, args []string) {
+ ctx := hctx.MakeContext()
+ config := hctx.GetConf(ctx)
+ fmt.Println(config.AiCompletion)
+ },
+}
+
var getBetaModeCmd = &cobra.Command{
Use: "beta-mode",
Short: "Enable beta-mode to opt-in to unreleased features",
@@ -109,4 +120,5 @@ func init() {
configGetCmd.AddCommand(getCustomColumnsCmd)
configGetCmd.AddCommand(getBetaModeCmd)
configGetCmd.AddCommand(getHighlightMatchesCmd)
+ configGetCmd.AddCommand(getEnableAiCompletion)
}
diff --git a/client/cmd/configSet.go b/client/cmd/configSet.go
index 85bb0fe..ab6ca06 100644
--- a/client/cmd/configSet.go
+++ b/client/cmd/configSet.go
@@ -70,6 +70,24 @@ var setBetaModeCommand = &cobra.Command{
},
}
+var setEnableAiCompletionCmd = &cobra.Command{
+ Use: "ai-completion",
+ Short: "Enable AI completion for searches starting with '?'",
+ Long: "Note that AI completion requests are sent to the shared hiSHtory backend and then to OpenAI. Requests are not logged, but still be careful not to put anything sensitive in queries.",
+ Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
+ ValidArgs: []string{"true", "false"},
+ Run: func(cmd *cobra.Command, args []string) {
+ val := args[0]
+ if val != "true" && val != "false" {
+ log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
+ }
+ ctx := hctx.MakeContext()
+ config := hctx.GetConf(ctx)
+ config.AiCompletion = (val == "true")
+ lib.CheckFatalError(hctx.SetConfig(config))
+ },
+}
+
var setHighlightMatchesCmd = &cobra.Command{
Use: "highlight-matches",
Short: "Enable highlight-matches to enable highlighting of matches in the search results",
@@ -119,4 +137,5 @@ func init() {
configSetCmd.AddCommand(setTimestampFormatCmd)
configSetCmd.AddCommand(setBetaModeCommand)
configSetCmd.AddCommand(setHighlightMatchesCmd)
+ configSetCmd.AddCommand(setEnableAiCompletionCmd)
}
diff --git a/client/cmd/install.go b/client/cmd/install.go
index 2b3b71b..e0a3f01 100644
--- a/client/cmd/install.go
+++ b/client/cmd/install.go
@@ -198,8 +198,8 @@ func handleDbUpgrades(ctx context.Context) error {
// Handles people running `hishtory update` from an old version of hishtory that
// doesn't support certain config options that we now default to true. This ensures
-// that upgrades get them enabled by default, but if someone has it explicitly disabled,
-// we keep it that way.
+// that we can customize the behavior for upgrades while still respecting the option
+// if someone has it explicitly set.
func handleUpgradedFeatures() error {
configContents, err := hctx.GetConfigContents()
if err != nil {
@@ -218,6 +218,10 @@ func handleUpgradedFeatures() error {
// highlighting is not yet configured, so enable it
config.HighlightMatches = true
}
+ if !strings.Contains(string(configContents), "ai_completion") {
+ // AI completion is not yet configured, disable it for upgrades since this is a new feature
+ config.AiCompletion = false
+ }
return hctx.SetConfig(&config)
}
@@ -562,6 +566,8 @@ func setup(userSecret string, isOffline bool) error {
config.IsEnabled = true
config.DeviceId = uuid.Must(uuid.NewRandom()).String()
config.ControlRSearchEnabled = true
+ // TODO: Set config.HighlightMatches = true here, so that we enable highlighting by default
+ config.AiCompletion = true
config.IsOffline = isOffline
err := hctx.SetConfig(&config)
if err != nil {
diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go
index b74598a..2b55ade 100644
--- a/client/hctx/hctx.go
+++ b/client/hctx/hctx.go
@@ -197,6 +197,8 @@ type ClientConfig struct {
BetaMode bool `json:"beta_mode"`
// Whether to highlight matches in search results
HighlightMatches bool `json:"highlight_matches"`
+ // Whether to enable AI completion
+ AiCompletion bool `json:"ai_completion"`
}
type CustomColumnDefinition struct {
diff --git a/client/lib/goldens/TestTui-AiQuery-Disabled b/client/lib/goldens/TestTui-AiQuery-Disabled
new file mode 100644
index 0000000..f9476c7
--- /dev/null
+++ b/client/lib/goldens/TestTui-AiQuery-Disabled
@@ -0,0 +1,27 @@
+Search Query: > ?myQuery
+
+┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+│ Hostname CWD Timestamp Runtime Exit Code Command │
+│────────────────────────────────────────────────────────────────────────────────────────────────────────│
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+hiSHtory: Search your shell history • ctrl+h help
\ No newline at end of file
diff --git a/client/tui/tui.go b/client/tui/tui.go
index 6029412..7c09f49 100644
--- a/client/tui/tui.go
+++ b/client/tui/tui.go
@@ -471,7 +471,7 @@ func getRowsFromAiSuggestions(ctx context.Context, columnNames []string, query s
func getRows(ctx context.Context, columnNames []string, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
db := hctx.GetDb(ctx)
config := hctx.GetConf(ctx)
- if config.BetaMode && strings.HasPrefix(query, "?") && len(query) > 1 {
+ if config.AiCompletion && !config.IsOffline && strings.HasPrefix(query, "?") && len(query) > 1 {
return getRowsFromAiSuggestions(ctx, columnNames, query)
}
searchResults, err := lib.Search(ctx, db, query, numEntries)