Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit f41b2ec

Browse files
committed
feat: enhance chat interface with comprehensive UI improvements
- Use bubbletea to implement a readline-like interface - Support arrow keys as well as the common readline keybindings for search - Implement /copy command for copying the latest response - Maintain backward compatibility with triple-quote formatting - When piping stdin into the run command, read the stdin and use it as the prompt Signed-off-by: Alberto Garcia Hierro <[email protected]>
1 parent 08a8afe commit f41b2ec

File tree

368 files changed

+64373
-88
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

368 files changed

+64373
-88
lines changed

commands/run.go

Lines changed: 280 additions & 81 deletions
Large diffs are not rendered by default.

desktop/desktop.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,9 @@ const (
371371
chatPrinterReasoning
372372
)
373373

374-
func (c *Client) Chat(backend, model, prompt, apiKey string) error {
374+
// Chat sends a chat message to the model, prints the response to the standard output
375+
// and then returns it as a slice of the received chunks.
376+
func (c *Client) Chat(backend, model, prompt, apiKey string) ([]string, error) {
375377
model = normalizeHuggingFaceModelName(model)
376378
if !strings.Contains(strings.Trim(model, "/"), "/") {
377379
// Do an extra API call to check if the model parameter isn't a model ID.
@@ -393,7 +395,7 @@ func (c *Client) Chat(backend, model, prompt, apiKey string) error {
393395

394396
jsonData, err := json.Marshal(reqBody)
395397
if err != nil {
396-
return fmt.Errorf("error marshaling request: %w", err)
398+
return nil, fmt.Errorf("error marshaling request: %w", err)
397399
}
398400

399401
var completionsPath string
@@ -411,18 +413,19 @@ func (c *Client) Chat(backend, model, prompt, apiKey string) error {
411413
apiKey,
412414
)
413415
if err != nil {
414-
return c.handleQueryError(err, completionsPath)
416+
return nil, c.handleQueryError(err, completionsPath)
415417
}
416418
defer resp.Body.Close()
417419

418420
if resp.StatusCode != http.StatusOK {
419421
body, _ := io.ReadAll(resp.Body)
420-
return fmt.Errorf("error response: status=%d body=%s", resp.StatusCode, body)
422+
return nil, fmt.Errorf("error response: status=%d body=%s", resp.StatusCode, body)
421423
}
422424

423425
printerState := chatPrinterNone
424426
reasoningFmt := color.New(color.FgWhite).Add(color.Italic)
425427
scanner := bufio.NewScanner(resp.Body)
428+
var chunks []string
426429
for scanner.Scan() {
427430
line := scanner.Text()
428431
if line == "" {
@@ -441,7 +444,7 @@ func (c *Client) Chat(backend, model, prompt, apiKey string) error {
441444

442445
var streamResp OpenAIChatResponse
443446
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
444-
return fmt.Errorf("error parsing stream response: %w", err)
447+
return nil, fmt.Errorf("error parsing stream response: %w", err)
445448
}
446449

447450
if len(streamResp.Choices) > 0 {
@@ -463,15 +466,16 @@ func (c *Client) Chat(backend, model, prompt, apiKey string) error {
463466
}
464467
printerState = chatPrinterContent
465468
fmt.Print(chunk)
469+
chunks = append(chunks, chunk)
466470
}
467471
}
468472
}
469473

470474
if err := scanner.Err(); err != nil {
471-
return fmt.Errorf("error reading response stream: %w", err)
475+
return nil, fmt.Errorf("error reading response stream: %w", err)
472476
}
473477

474-
return nil
478+
return chunks, nil
475479
}
476480

477481
func (c *Client) Remove(models []string, force bool) (string, error) {

go.mod

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ go 1.24
55
toolchain go1.24.4
66

77
require (
8+
github.com/charmbracelet/bubbles v0.21.0
9+
github.com/charmbracelet/bubbletea v1.3.6
810
github.com/containerd/errdefs v1.0.0
911
github.com/docker/cli v28.3.0+incompatible
1012
github.com/docker/cli-docs-tool v0.10.0
@@ -24,14 +26,23 @@ require (
2426
github.com/stretchr/testify v1.10.0
2527
go.opentelemetry.io/otel v1.37.0
2628
go.uber.org/mock v0.5.0
29+
golang.design/x/clipboard v0.7.1
2730
golang.org/x/sync v0.15.0
31+
golang.org/x/term v0.32.0
2832
)
2933

3034
require (
3135
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
3236
github.com/Microsoft/go-winio v0.6.2 // indirect
3337
github.com/StackExchange/wmi v1.2.1 // indirect
38+
github.com/atotto/clipboard v0.1.4 // indirect
39+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
3440
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
41+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
42+
github.com/charmbracelet/lipgloss v1.1.0 // indirect
43+
github.com/charmbracelet/x/ansi v0.9.3 // indirect
44+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
45+
github.com/charmbracelet/x/term v0.2.1 // indirect
3546
github.com/containerd/containerd/v2 v2.1.3 // indirect
3647
github.com/containerd/errdefs/pkg v0.3.0 // indirect
3748
github.com/containerd/log v0.1.0 // indirect
@@ -46,6 +57,7 @@ require (
4657
github.com/docker/docker-credential-helpers v0.9.3 // indirect
4758
github.com/elastic/go-sysinfo v1.15.3 // indirect
4859
github.com/elastic/go-windows v1.0.2 // indirect
60+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
4961
github.com/felixge/httpsnoop v1.0.4 // indirect
5062
github.com/fsnotify/fsnotify v1.9.0 // indirect
5163
github.com/fvbommel/sortorder v1.1.0 // indirect
@@ -63,7 +75,9 @@ require (
6375
github.com/jaypipes/pcidb v1.0.1 // indirect
6476
github.com/json-iterator/go v1.1.12 // indirect
6577
github.com/klauspost/compress v1.18.0 // indirect
78+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
6679
github.com/mattn/go-colorable v0.1.13 // indirect
80+
github.com/mattn/go-localereader v0.0.1 // indirect
6781
github.com/mattn/go-runewidth v0.0.16 // indirect
6882
github.com/mattn/go-shellwords v1.0.12 // indirect
6983
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -75,6 +89,9 @@ require (
7589
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
7690
github.com/modern-go/reflect2 v1.0.2 // indirect
7791
github.com/morikuni/aec v1.0.0 // indirect
92+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
93+
github.com/muesli/cancelreader v0.2.2 // indirect
94+
github.com/muesli/termenv v0.16.0 // indirect
7895
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
7996
github.com/opencontainers/go-digest v1.0.0 // indirect
8097
github.com/opencontainers/image-spec v1.1.1 // indirect
@@ -89,6 +106,7 @@ require (
89106
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
90107
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a // indirect
91108
github.com/vbatts/tar-split v0.12.1 // indirect
109+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
92110
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
93111
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
94112
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect
@@ -101,6 +119,9 @@ require (
101119
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
102120
golang.org/x/crypto v0.39.0 // indirect
103121
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
122+
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
123+
golang.org/x/image v0.28.0 // indirect
124+
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
104125
golang.org/x/mod v0.25.0 // indirect
105126
golang.org/x/net v0.41.0 // indirect
106127
golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH
1010
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
1111
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
1212
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
13+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
14+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
15+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
16+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
1317
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1418
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
1519
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
@@ -22,6 +26,20 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
2226
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
2327
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
2428
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
29+
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
30+
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
31+
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
32+
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
33+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
34+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
35+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
36+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
37+
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
38+
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
39+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
40+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
41+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
42+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
2543
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
2644
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
2745
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
@@ -87,6 +105,8 @@ github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn
87105
github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo=
88106
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
89107
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
108+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
109+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
90110
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
91111
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
92112
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
@@ -162,12 +182,16 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
162182
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
163183
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
164184
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
185+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
186+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
165187
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
166188
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
167189
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
168190
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
169191
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
170192
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
193+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
194+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
171195
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
172196
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
173197
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -202,6 +226,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
202226
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
203227
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
204228
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
229+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
230+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
231+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
232+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
233+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
234+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
205235
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
206236
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
207237
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -273,6 +303,8 @@ github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ
273303
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw=
274304
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
275305
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
306+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
307+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
276308
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
277309
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
278310
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
@@ -305,6 +337,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
305337
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
306338
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
307339
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
340+
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
341+
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
308342
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
309343
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
310344
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -315,6 +349,12 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
315349
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
316350
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
317351
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
352+
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
353+
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
354+
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
355+
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
356+
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
357+
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
318358
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
319359
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
320360
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
@@ -341,6 +381,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
341381
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
342382
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
343383
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
384+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
344385
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
345386
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
346387
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

pkg/history/history.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Package history provides a command history stored inside the Docker CLI configuration.
2+
package history
3+
4+
import (
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/docker/cli/cli/command"
10+
)
11+
12+
const MaxHistoryLength = 100
13+
14+
type History struct {
15+
configPath string
16+
history []string
17+
}
18+
19+
// New creates a new History instance and loads all previous history, if it exists.
20+
func New(cli *command.DockerCli) (*History, error) {
21+
dirname := filepath.Dir(cli.ConfigFile().Filename)
22+
p := filepath.Join(dirname, "model-cli", "history.txt")
23+
h := &History{configPath: p}
24+
if err := h.load(); err != nil {
25+
return nil, err
26+
}
27+
return h, nil
28+
}
29+
30+
func (h *History) load() error {
31+
data, err := os.ReadFile(h.configPath)
32+
if err != nil {
33+
if os.IsNotExist(err) {
34+
return nil
35+
}
36+
return err
37+
}
38+
39+
var history []string
40+
seen := make(map[string]bool)
41+
for line := range strings.SplitSeq(strings.TrimSuffix(string(data), "\n"), "\n") {
42+
if !seen[line] {
43+
history = append(history, line)
44+
seen[line] = true
45+
}
46+
}
47+
h.history = history
48+
return nil
49+
}
50+
51+
// Append adds a new entry to the history and updates the history file.
52+
func (h *History) Append(question string) error {
53+
if strings.Contains(question, "\n") {
54+
return nil
55+
}
56+
57+
h.history = append(h.history, question)
58+
if len(h.history) > MaxHistoryLength {
59+
h.history = h.history[len(h.history)-MaxHistoryLength:]
60+
}
61+
buf := strings.Join(h.history, "\n")
62+
63+
if err := os.MkdirAll(filepath.Dir(h.configPath), 0700); err != nil {
64+
return err
65+
}
66+
67+
if err := os.WriteFile(h.configPath+".tmp", []byte(buf), 0600); err != nil {
68+
return err
69+
}
70+
_ = os.Remove(h.configPath)
71+
return os.Rename(h.configPath+".tmp", h.configPath)
72+
}
73+
74+
// Suggestions returns a list of suggested inputs based on the current input.
75+
func (h *History) Suggestions(text string) []string {
76+
var suggestions []string
77+
78+
text = strings.ToLower(text)
79+
for _, line := range h.history {
80+
if strings.HasPrefix(strings.ToLower(line), text) {
81+
suggestions = append(suggestions, line)
82+
}
83+
}
84+
85+
return suggestions
86+
}
87+
88+
// Previous returns the previous input in the history based on the current input and cursor position.
89+
func (h *History) Previous(text string, cursorPosition int, from int) (int, string) {
90+
n := len(h.history)
91+
text = strings.ToLower(text[0:cursorPosition])
92+
for dec := range n - 1 {
93+
index := mod(from-dec-1, n)
94+
line := h.history[index]
95+
if strings.HasPrefix(strings.ToLower(line), text) {
96+
return index, line
97+
}
98+
}
99+
return from, text
100+
}
101+
102+
// Next returns the next input in the history based on the current input and cursor position.
103+
func (h *History) Next(text string, cursorPosition int, from int) (int, string) {
104+
n := len(h.history)
105+
text = strings.ToLower(text[0:cursorPosition])
106+
for inc := range n - 1 {
107+
index := mod(from+inc+1, n)
108+
line := h.history[index]
109+
if strings.HasPrefix(strings.ToLower(line), text) {
110+
return index, line
111+
}
112+
}
113+
return from, text
114+
}
115+
116+
func mod(a, b int) int {
117+
return (a%b + b) % b
118+
}

0 commit comments

Comments
 (0)