on main help sign in
Merge remote-tracking branch 'origin/main' into server-client-2
ee13558Ayman Bagabas
.github/workflows/security.yml+6−6
@@ -30,11 +30,11 @@ jobs:
30 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31 with:
31 with:
32 persist-credentials: false
32 persist-credentials: false
33− - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
33+ - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
34 with:
34 with:
35 languages: ${{ matrix.language }}
35 languages: ${{ matrix.language }}
36− - uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
36+ - uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
37− - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
37+ - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
38
38
39 grype:
39 grype:
40 runs-on: ubuntu-latest
40 runs-on: ubuntu-latest
@@ -52,7 +52,7 @@ jobs:
52 path: "."
52 path: "."
53 fail-build: true
53 fail-build: true
54 severity-cutoff: critical
54 severity-cutoff: critical
55− - uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
55+ - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
56 with:
56 with:
57 sarif_file: ${{ steps.scan.outputs.sarif }}
57 sarif_file: ${{ steps.scan.outputs.sarif }}
58
58
@@ -73,7 +73,7 @@ jobs:
73 - name: Run govulncheck
73 - name: Run govulncheck
74 run: |
74 run: |
75 govulncheck -C . -format sarif ./... > results.sarif
75 govulncheck -C . -format sarif ./... > results.sarif
76− - uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
76+ - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
77 with:
77 with:
78 sarif_file: results.sarif
78 sarif_file: results.sarif
79
79
@@ -86,7 +86,7 @@ jobs:
86 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
86 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
87 with:
87 with:
88 persist-credentials: false
88 persist-credentials: false
89− - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
89+ - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
90 with:
90 with:
91 fail-on-severity: critical
91 fail-on-severity: critical
92 allow-licenses: BSD-2-Clause, BSD-3-Clause, MIT, MIT-0, Apache-2.0, MPL-2.0, ISC, LicenseRef-scancode-google-patent-license-golang, Unlicense
92 allow-licenses: BSD-2-Clause, BSD-3-Clause, MIT, MIT-0, Apache-2.0, MPL-2.0, ISC, LicenseRef-scancode-google-patent-license-golang, Unlicense
Taskfile.yaml+5
@@ -184,3 +184,8 @@ tasks:
184 - go get charm.land/fantasy
184 - go get charm.land/fantasy
185 - go get charm.land/catwalk
185 - go get charm.land/catwalk
186 - go mod tidy
186 - go mod tidy
187+
188+ sqlc:
189+ desc: Generate code using SQLC
190+ cmds:
191+ - sqlc generate
go.mod+1−1
@@ -5,7 +5,7 @@ go 1.26.1
5require (
5require (
6 charm.land/bubbles/v2 v2.0.0
6 charm.land/bubbles/v2 v2.0.0
7 charm.land/bubbletea/v2 v2.0.2
7 charm.land/bubbletea/v2 v2.0.2
8− charm.land/catwalk v0.30.0
8+ charm.land/catwalk v0.30.3
9 charm.land/fang/v2 v2.0.1
9 charm.land/fang/v2 v2.0.1
10 charm.land/fantasy v0.12.3
10 charm.land/fantasy v0.12.3
11 charm.land/glamour/v2 v2.0.0
11 charm.land/glamour/v2 v2.0.0
go.sum+2−2
@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
2charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
2charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
3charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
3charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
4charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
4charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
5−charm.land/catwalk v0.30.0 h1:CgWN3hM0HYYXlxMFsd1mfpBliqvx9gH3LolVi1VfMSU=
5+charm.land/catwalk v0.30.3 h1:eCRwVoi1znrNGYiPZoBIbWt8+Q4kDhT3zziqnPO3s2Y=
6−charm.land/catwalk v0.30.0/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
6+charm.land/catwalk v0.30.3/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
7charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
7charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
8charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
8charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
9charm.land/fantasy v0.12.3 h1:gvqRWD7vWmpNN0VcQ+rSku5QdTlLegqrlJDVCDdAh58=
9charm.land/fantasy v0.12.3 h1:gvqRWD7vWmpNN0VcQ+rSku5QdTlLegqrlJDVCDdAh58=
internal/app/app.go+39−3
@@ -171,9 +171,40 @@ func (app *App) AgentNotifications() *pubsub.Broker[notify.Notification] {
171 return app.agentNotifications
171 return app.agentNotifications
172}
172}
173
173
174+// resolveSession resolves which session to use for a non-interactive run
175+// If continueSessionID is set, it looks up that session by ID
176+// If useLast is set, it returns the most recently updated top-level session
177+// Otherwise, it creates a new session
178+func (app *App) resolveSession(ctx context.Context, continueSessionID string, useLast bool) (session.Session, error) {
179+ switch {
180+ case continueSessionID != "":
181+ if app.Sessions.IsAgentToolSession(continueSessionID) {
182+ return session.Session{}, fmt.Errorf("cannot continue an agent tool session: %s", continueSessionID)
183+ }
184+ sess, err := app.Sessions.Get(ctx, continueSessionID)
185+ if err != nil {
186+ return session.Session{}, fmt.Errorf("session not found: %s", continueSessionID)
187+ }
188+ if sess.ParentSessionID != "" {
189+ return session.Session{}, fmt.Errorf("cannot continue a child session: %s", continueSessionID)
190+ }
191+ return sess, nil
192+
193+ case useLast:
194+ sess, err := app.Sessions.GetLast(ctx)
195+ if err != nil {
196+ return session.Session{}, fmt.Errorf("no sessions found to continue")
197+ }
198+ return sess, nil
199+
200+ default:
201+ return app.Sessions.Create(ctx, agent.DefaultSessionName)
202+ }
203+}
204+
174// RunNonInteractive runs the application in non-interactive mode with the
205// RunNonInteractive runs the application in non-interactive mode with the
175// given prompt, printing to stdout.
206// given prompt, printing to stdout.
176−func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
207+func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool, continueSessionID string, useLast bool) error {
177 slog.Info("Running in non-interactive mode")
208 slog.Info("Running in non-interactive mode")
178
209
179 ctx, cancel := context.WithCancel(ctx)
210 ctx, cancel := context.WithCancel(ctx)
@@ -241,11 +272,16 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
241
272
242 defer stopSpinner()
273 defer stopSpinner()
243
274
244− sess, err := app.Sessions.Create(ctx, agent.DefaultSessionName)
275+ sess, err := app.resolveSession(ctx, continueSessionID, useLast)
245 if err != nil {
276 if err != nil {
246 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
277 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
247 }
278 }
279+
280+ if continueSessionID != "" || useLast {
281+ slog.Info("Continuing session for non-interactive run", "session_id", sess.ID)
282+ } else {
248− slog.Info("Created session for non-interactive run", "session_id", sess.ID)
283+ slog.Info("Created session for non-interactive run", "session_id", sess.ID)
284+ }
249
285
250 // Automatically approve all permission requests for this non-interactive
286 // Automatically approve all permission requests for this non-interactive
251 // session.
287 // session.
internal/app/resolve_session_test.gonew file+174
@@ -0,0 +1,174 @@
1+package app
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "fmt"
7+ "strings"
8+ "testing"
9+
10+ "github.com/charmbracelet/crush/internal/pubsub"
11+ "github.com/charmbracelet/crush/internal/session"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+// mockSessionService is a minimal mock of session.Service for testing resolveSession.
16+type mockSessionService struct {
17+ sessions []session.Session
18+ created []session.Session
19+}
20+
21+func (m *mockSessionService) Subscribe(context.Context) <-chan pubsub.Event[session.Session] {
22+ return make(chan pubsub.Event[session.Session])
23+}
24+
25+func (m *mockSessionService) Create(_ context.Context, title string) (session.Session, error) {
26+ s := session.Session{ID: "new-session-id", Title: title}
27+ m.created = append(m.created, s)
28+ return s, nil
29+}
30+
31+func (m *mockSessionService) CreateTitleSession(context.Context, string) (session.Session, error) {
32+ return session.Session{}, nil
33+}
34+
35+func (m *mockSessionService) CreateTaskSession(context.Context, string, string, string) (session.Session, error) {
36+ return session.Session{}, nil
37+}
38+
39+func (m *mockSessionService) Get(_ context.Context, id string) (session.Session, error) {
40+ for _, s := range m.sessions {
41+ if s.ID == id {
42+ return s, nil
43+ }
44+ }
45+ return session.Session{}, sql.ErrNoRows
46+}
47+
48+func (m *mockSessionService) GetLast(_ context.Context) (session.Session, error) {
49+ if len(m.sessions) > 0 {
50+ return m.sessions[0], nil
51+ }
52+ return session.Session{}, sql.ErrNoRows
53+}
54+
55+func (m *mockSessionService) List(context.Context) ([]session.Session, error) {
56+ return m.sessions, nil
57+}
58+
59+func (m *mockSessionService) Save(_ context.Context, s session.Session) (session.Session, error) {
60+ return s, nil
61+}
62+
63+func (m *mockSessionService) UpdateTitleAndUsage(context.Context, string, string, int64, int64, float64) error {
64+ return nil
65+}
66+
67+func (m *mockSessionService) Rename(context.Context, string, string) error {
68+ return nil
69+}
70+
71+func (m *mockSessionService) Delete(context.Context, string) error {
72+ return nil
73+}
74+
75+func (m *mockSessionService) CreateAgentToolSessionID(messageID, toolCallID string) string {
76+ return fmt.Sprintf("%s$$%s", messageID, toolCallID)
77+}
78+
79+func (m *mockSessionService) ParseAgentToolSessionID(sessionID string) (string, string, bool) {
80+ parts := strings.Split(sessionID, "$$")
81+ if len(parts) != 2 {
82+ return "", "", false
83+ }
84+ return parts[0], parts[1], true
85+}
86+
87+func (m *mockSessionService) IsAgentToolSession(sessionID string) bool {
88+ _, _, ok := m.ParseAgentToolSessionID(sessionID)
89+ return ok
90+}
91+
92+func newTestApp(sessions session.Service) *App {
93+ return &App{Sessions: sessions}
94+}
95+
96+func TestResolveSession_NewSession(t *testing.T) {
97+ mock := &mockSessionService{}
98+ app := newTestApp(mock)
99+
100+ sess, err := app.resolveSession(t.Context(), "", false)
101+ require.NoError(t, err)
102+ require.Equal(t, "new-session-id", sess.ID)
103+ require.Len(t, mock.created, 1)
104+}
105+
106+func TestResolveSession_ContinueByID(t *testing.T) {
107+ mock := &mockSessionService{
108+ sessions: []session.Session{
109+ {ID: "existing-id", Title: "Old session"},
110+ },
111+ }
112+ app := newTestApp(mock)
113+
114+ sess, err := app.resolveSession(t.Context(), "existing-id", false)
115+ require.NoError(t, err)
116+ require.Equal(t, "existing-id", sess.ID)
117+ require.Equal(t, "Old session", sess.Title)
118+ require.Empty(t, mock.created)
119+}
120+
121+func TestResolveSession_ContinueByID_NotFound(t *testing.T) {
122+ mock := &mockSessionService{}
123+ app := newTestApp(mock)
124+
125+ _, err := app.resolveSession(t.Context(), "nonexistent", false)
126+ require.Error(t, err)
127+ require.Contains(t, err.Error(), "session not found")
128+}
129+
130+func TestResolveSession_ContinueByID_ChildSession(t *testing.T) {
131+ mock := &mockSessionService{
132+ sessions: []session.Session{
133+ {ID: "child-id", ParentSessionID: "parent-id", Title: "Child session"},
134+ },
135+ }
136+ app := newTestApp(mock)
137+
138+ _, err := app.resolveSession(t.Context(), "child-id", false)
139+ require.Error(t, err)
140+ require.Contains(t, err.Error(), "cannot continue a child session")
141+}
142+
143+func TestResolveSession_ContinueByID_AgentToolSession(t *testing.T) {
144+ mock := &mockSessionService{}
145+ app := newTestApp(mock)
146+
147+ _, err := app.resolveSession(t.Context(), "msg123$$tool456", false)
148+ require.Error(t, err)
149+ require.Contains(t, err.Error(), "cannot continue an agent tool session")
150+}
151+
152+func TestResolveSession_Last(t *testing.T) {
153+ mock := &mockSessionService{
154+ sessions: []session.Session{
155+ {ID: "most-recent", Title: "Latest session"},
156+ {ID: "older", Title: "Older session"},
157+ },
158+ }
159+ app := newTestApp(mock)
160+
161+ sess, err := app.resolveSession(t.Context(), "", true)
162+ require.NoError(t, err)
163+ require.Equal(t, "most-recent", sess.ID)
164+ require.Empty(t, mock.created)
165+}
166+
167+func TestResolveSession_Last_NoSessions(t *testing.T) {
168+ mock := &mockSessionService{}
169+ app := newTestApp(mock)
170+
171+ _, err := app.resolveSession(t.Context(), "", true)
172+ require.Error(t, err)
173+ require.Contains(t, err.Error(), "no sessions found")
174+}
internal/cmd/run.go+108−8
@@ -17,6 +17,7 @@ import (
17 "github.com/charmbracelet/crush/internal/format"
17 "github.com/charmbracelet/crush/internal/format"
18 "github.com/charmbracelet/crush/internal/proto"
18 "github.com/charmbracelet/crush/internal/proto"
19 "github.com/charmbracelet/crush/internal/pubsub"
19 "github.com/charmbracelet/crush/internal/pubsub"
20+ "github.com/charmbracelet/crush/internal/session"
20 "github.com/charmbracelet/crush/internal/ui/anim"
21 "github.com/charmbracelet/crush/internal/ui/anim"
21 "github.com/charmbracelet/crush/internal/ui/styles"
22 "github.com/charmbracelet/crush/internal/ui/styles"
22 "github.com/charmbracelet/x/ansi"
23 "github.com/charmbracelet/x/ansi"
@@ -48,12 +49,23 @@ crush run --quiet "Generate a README for this project"
48
49
49# Run in verbose mode (show logs)
50# Run in verbose mode (show logs)
50crush run --verbose "Generate a README for this project"
51crush run --verbose "Generate a README for this project"
52+
53+# Continue a previous session
54+crush run --session {session-id} "Follow up on your last response"
55+
56+# Continue the most recent session
57+crush run --continue "Follow up on your last response"
58+
51 `,
59 `,
52 RunE: func(cmd *cobra.Command, args []string) error {
60 RunE: func(cmd *cobra.Command, args []string) error {
61+ var (
53− quiet, _ := cmd.Flags().GetBool("quiet")
62+ quiet, _ = cmd.Flags().GetBool("quiet")
54− verbose, _ := cmd.Flags().GetBool("verbose")
63+ verbose, _ = cmd.Flags().GetBool("verbose")
55− largeModel, _ := cmd.Flags().GetString("model")
64+ largeModel, _ = cmd.Flags().GetString("model")
56− smallModel, _ := cmd.Flags().GetString("small-model")
65+ smallModel, _ = cmd.Flags().GetString("small-model")
66+ sessionID, _ = cmd.Flags().GetString("session")
67+ useLast, _ = cmd.Flags().GetBool("continue")
68+ )
57
69
58 // Cancel on SIGINT or SIGTERM.
70 // Cancel on SIGINT or SIGTERM.
59 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
71 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
@@ -65,6 +77,14 @@ crush run --verbose "Generate a README for this project"
65 }
77 }
66 defer cleanup()
78 defer cleanup()
67
79
80+ if sessionID != "" {
81+ sess, err := resolveSessionByID(ctx, c, ws.ID, sessionID)
82+ if err != nil {
83+ return err
84+ }
85+ sessionID = sess.ID
86+ }
87+
68 if !ws.Config.IsConfigured() {
88 if !ws.Config.IsConfigured() {
69 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
89 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
70 }
90 }
@@ -88,7 +108,14 @@ crush run --verbose "Generate a README for this project"
88 event.SetNonInteractive(true)
108 event.SetNonInteractive(true)
89 event.AppInitialized()
109 event.AppInitialized()
90
110
111+ switch {
112+ case sessionID != "":
113+ event.SetContinueBySessionID(true)
114+ case useLast:
115+ event.SetContinueLastSession(true)
116+ }
117+
91− return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose)
118+ return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose, sessionID, useLast)
92 },
119 },
93}
120}
94
121
@@ -97,6 +124,9 @@ func init() {
97 runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
124 runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
98 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
125 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
99 runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
126 runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
127+ runCmd.Flags().StringP("session", "s", "", "Continue a previous session by ID")
128+ runCmd.Flags().BoolP("continue", "C", false, "Continue the most recent session")
129+ runCmd.MarkFlagsMutuallyExclusive("session", "continue")
100}
130}
101
131
102// runNonInteractive executes the agent via the server and streams output
132// runNonInteractive executes the agent via the server and streams output
@@ -107,6 +137,8 @@ func runNonInteractive(
107 ws *proto.Workspace,
137 ws *proto.Workspace,
108 prompt, largeModel, smallModel string,
138 prompt, largeModel, smallModel string,
109 hideSpinner bool,
139 hideSpinner bool,
140+ continueSessionID string,
141+ useLast bool,
110) error {
142) error {
111 slog.Info("Running in non-interactive mode")
143 slog.Info("Running in non-interactive mode")
112
144
@@ -172,11 +204,15 @@ func runNonInteractive(
172
204
173 defer stopSpinner()
205 defer stopSpinner()
174
206
175− sess, err := c.CreateSession(ctx, ws.ID, "non-interactive")
207+ sess, err := resolveSession(ctx, c, ws.ID, continueSessionID, useLast)
176 if err != nil {
208 if err != nil {
177− return fmt.Errorf("failed to create session: %w", err)
209+ return fmt.Errorf("failed to resolve session: %w", err)
210+ }
211+ if continueSessionID != "" || useLast {
212+ slog.Info("Continuing session for non-interactive run", "session_id", sess.ID)
213+ } else {
214+ slog.Info("Created session for non-interactive run", "session_id", sess.ID)
178 }
215 }
179− slog.Info("Created session for non-interactive run", "session_id", sess.ID)
180
216
181 events, err := c.SubscribeEvents(ctx, ws.ID)
217 events, err := c.SubscribeEvents(ctx, ws.ID)
182 if err != nil {
218 if err != nil {
@@ -407,3 +443,67 @@ func validateModelMatches(matches []modelMatch, modelID, label string) (modelMat
407 }
443 }
408 return matches[0], nil
444 return matches[0], nil
409}
445}
446+
447+// resolveSession returns the session to use for a non-interactive run.
448+// If continueSessionID is set it fetches that session; if useLast is set it
449+// returns the most recently updated top-level session; otherwise it creates a
450+// new one.
451+func resolveSession(ctx context.Context, c *client.Client, wsID, continueSessionID string, useLast bool) (*session.Session, error) {
452+ switch {
453+ case continueSessionID != "":
454+ sess, err := c.GetSession(ctx, wsID, continueSessionID)
455+ if err != nil {
456+ return nil, fmt.Errorf("session not found: %s", continueSessionID)
457+ }
458+ if sess.ParentSessionID != "" {
459+ return nil, fmt.Errorf("cannot continue a child session: %s", continueSessionID)
460+ }
461+ return sess, nil
462+
463+ case useLast:
464+ sessions, err := c.ListSessions(ctx, wsID)
465+ if err != nil || len(sessions) == 0 {
466+ return nil, fmt.Errorf("no sessions found to continue")
467+ }
468+ last := sessions[0]
469+ for _, s := range sessions[1:] {
470+ if s.UpdatedAt > last.UpdatedAt && s.ParentSessionID == "" {
471+ last = s
472+ }
473+ }
474+ return &last, nil
475+
476+ default:
477+ return c.CreateSession(ctx, wsID, "non-interactive")
478+ }
479+}
480+
481+// resolveSessionByID resolves a session ID that may be a full UUID or a hash
482+// prefix returned by crush session list.
483+func resolveSessionByID(ctx context.Context, c *client.Client, wsID, id string) (*session.Session, error) {
484+ if sess, err := c.GetSession(ctx, wsID, id); err == nil {
485+ return sess, nil
486+ }
487+
488+ sessions, err := c.ListSessions(ctx, wsID)
489+ if err != nil {
490+ return nil, err
491+ }
492+
493+ var matches []session.Session
494+ for _, s := range sessions {
495+ hash := session.HashID(s.ID)
496+ if hash == id || strings.HasPrefix(hash, id) {
497+ matches = append(matches, s)
498+ }
499+ }
500+
501+ switch len(matches) {
502+ case 0:
503+ return nil, fmt.Errorf("session %q not found", id)
504+ case 1:
505+ return &matches[0], nil
506+ default:
507+ return nil, fmt.Errorf("session ID %q is ambiguous (%d matches)", id, len(matches))
508+ }
509+}
internal/cmd/session.go+16
@@ -17,6 +17,7 @@ import (
17 "github.com/charmbracelet/colorprofile"
17 "github.com/charmbracelet/colorprofile"
18 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/config"
19 "github.com/charmbracelet/crush/internal/db"
19 "github.com/charmbracelet/crush/internal/db"
20+ "github.com/charmbracelet/crush/internal/event"
20 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/session"
22 "github.com/charmbracelet/crush/internal/session"
22 "github.com/charmbracelet/crush/internal/ui/chat"
23 "github.com/charmbracelet/crush/internal/ui/chat"
@@ -126,6 +127,9 @@ func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func()
126}
127}
127
128
128func runSessionList(cmd *cobra.Command, _ []string) error {
129func runSessionList(cmd *cobra.Command, _ []string) error {
130+ event.SetNonInteractive(true)
131+ event.SessionListed(sessionListJSON)
132+
129 ctx, svc, cleanup, err := sessionSetup(cmd)
133 ctx, svc, cleanup, err := sessionSetup(cmd)
130 if err != nil {
134 if err != nil {
131 return err
135 return err
@@ -249,6 +253,9 @@ func resolveSessionID(ctx context.Context, svc session.Service, id string) (sess
249}
253}
250
254
251func runSessionShow(cmd *cobra.Command, args []string) error {
255func runSessionShow(cmd *cobra.Command, args []string) error {
256+ event.SetNonInteractive(true)
257+ event.SessionShown(sessionShowJSON)
258+
252 ctx, svc, cleanup, err := sessionSetup(cmd)
259 ctx, svc, cleanup, err := sessionSetup(cmd)
253 if err != nil {
260 if err != nil {
254 return err
261 return err
@@ -273,6 +280,9 @@ func runSessionShow(cmd *cobra.Command, args []string) error {
273}
280}
274
281
275func runSessionDelete(cmd *cobra.Command, args []string) error {
282func runSessionDelete(cmd *cobra.Command, args []string) error {
283+ event.SetNonInteractive(true)
284+ event.SessionDeletedCommand(sessionDeleteJSON)
285+
276 ctx, svc, cleanup, err := sessionSetup(cmd)
286 ctx, svc, cleanup, err := sessionSetup(cmd)
277 if err != nil {
287 if err != nil {
278 return err
288 return err
@@ -305,6 +315,9 @@ func runSessionDelete(cmd *cobra.Command, args []string) error {
305}
315}
306
316
307func runSessionRename(cmd *cobra.Command, args []string) error {
317func runSessionRename(cmd *cobra.Command, args []string) error {
318+ event.SetNonInteractive(true)
319+ event.SessionRenamed(sessionRenameJSON)
320+
308 ctx, svc, cleanup, err := sessionSetup(cmd)
321 ctx, svc, cleanup, err := sessionSetup(cmd)
309 if err != nil {
322 if err != nil {
310 return err
323 return err
@@ -338,6 +351,9 @@ func runSessionRename(cmd *cobra.Command, args []string) error {
338}
351}
339
352
340func runSessionLast(cmd *cobra.Command, _ []string) error {
353func runSessionLast(cmd *cobra.Command, _ []string) error {
354+ event.SetNonInteractive(true)
355+ event.SessionLastShown(sessionLastJSON)
356+
341 ctx, svc, cleanup, err := sessionSetup(cmd)
357 ctx, svc, cleanup, err := sessionSetup(cmd)
342 if err != nil {
358 if err != nil {
343 return err
359 return err
internal/config/config.go+6
@@ -592,6 +592,12 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
592 return nil
592 return nil
593 }
593 }
594 return errors.New("not a valid bedrock api key")
594 return errors.New("not a valid bedrock api key")
595+ case catwalk.TypeVercel:
596+ // NOTE: Vercel does not validate API keys on the `/models` endpoint.
597+ if strings.HasPrefix(apiKey, "vck_") { // Vercel API keys
598+ return nil
599+ }
600+ return errors.New("not a valid vercel api key")
595 }
601 }
596
602
597 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
603 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
internal/db/db.go+10
@@ -63,6 +63,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
63 if q.getHourDayHeatmapStmt, err = db.PrepareContext(ctx, getHourDayHeatmap); err != nil {
63 if q.getHourDayHeatmapStmt, err = db.PrepareContext(ctx, getHourDayHeatmap); err != nil {
64 return nil, fmt.Errorf("error preparing query GetHourDayHeatmap: %w", err)
64 return nil, fmt.Errorf("error preparing query GetHourDayHeatmap: %w", err)
65 }
65 }
66+ if q.getLastSessionStmt, err = db.PrepareContext(ctx, getLastSession); err != nil {
67+ return nil, fmt.Errorf("error preparing query GetLastSession: %w", err)
68+ }
66 if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
69 if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
67 return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
70 return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
68 }
71 }
@@ -202,6 +205,11 @@ func (q *Queries) Close() error {
202 err = fmt.Errorf("error closing getHourDayHeatmapStmt: %w", cerr)
205 err = fmt.Errorf("error closing getHourDayHeatmapStmt: %w", cerr)
203 }
206 }
204 }
207 }
208+ if q.getLastSessionStmt != nil {
209+ if cerr := q.getLastSessionStmt.Close(); cerr != nil {
210+ err = fmt.Errorf("error closing getLastSessionStmt: %w", cerr)
211+ }
212+ }
205 if q.getMessageStmt != nil {
213 if q.getMessageStmt != nil {
206 if cerr := q.getMessageStmt.Close(); cerr != nil {
214 if cerr := q.getMessageStmt.Close(); cerr != nil {
207 err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
215 err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
@@ -369,6 +377,7 @@ type Queries struct {
369 getFileByPathAndSessionStmt *sql.Stmt
377 getFileByPathAndSessionStmt *sql.Stmt
370 getFileReadStmt *sql.Stmt
378 getFileReadStmt *sql.Stmt
371 getHourDayHeatmapStmt *sql.Stmt
379 getHourDayHeatmapStmt *sql.Stmt
380+ getLastSessionStmt *sql.Stmt
372 getMessageStmt *sql.Stmt
381 getMessageStmt *sql.Stmt
373 getRecentActivityStmt *sql.Stmt
382 getRecentActivityStmt *sql.Stmt
374 getSessionByIDStmt *sql.Stmt
383 getSessionByIDStmt *sql.Stmt
@@ -411,6 +420,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
411 getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
420 getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
412 getFileReadStmt: q.getFileReadStmt,
421 getFileReadStmt: q.getFileReadStmt,
413 getHourDayHeatmapStmt: q.getHourDayHeatmapStmt,
422 getHourDayHeatmapStmt: q.getHourDayHeatmapStmt,
423+ getLastSessionStmt: q.getLastSessionStmt,
414 getMessageStmt: q.getMessageStmt,
424 getMessageStmt: q.getMessageStmt,
415 getRecentActivityStmt: q.getRecentActivityStmt,
425 getRecentActivityStmt: q.getRecentActivityStmt,
416 getSessionByIDStmt: q.getSessionByIDStmt,
426 getSessionByIDStmt: q.getSessionByIDStmt,
internal/db/querier.go+1
@@ -22,6 +22,7 @@ type Querier interface {
22 GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
22 GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
23 GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
23 GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
24 GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
24 GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
25+ GetLastSession(ctx context.Context) (Session, error)
25 GetMessage(ctx context.Context, id string) (Message, error)
26 GetMessage(ctx context.Context, id string) (Message, error)
26 GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
27 GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
27 GetSessionByID(ctx context.Context, id string) (Session, error)
28 GetSessionByID(ctx context.Context, id string) (Session, error)
internal/db/sessions.sql.go+26
@@ -83,6 +83,32 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error {
83 return err
83 return err
84}
84}
85
85
86+const getLastSession = `-- name: GetLastSession :one
87+SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
88+FROM sessions
89+ORDER BY updated_at DESC
90+LIMIT 1
91+`
92+
93+func (q *Queries) GetLastSession(ctx context.Context) (Session, error) {
94+ row := q.queryRow(ctx, q.getLastSessionStmt, getLastSession)
95+ var i Session
96+ err := row.Scan(
97+ &i.ID,
98+ &i.ParentSessionID,
99+ &i.Title,
100+ &i.MessageCount,
101+ &i.PromptTokens,
102+ &i.CompletionTokens,
103+ &i.Cost,
104+ &i.UpdatedAt,
105+ &i.CreatedAt,
106+ &i.SummaryMessageID,
107+ &i.Todos,
108+ )
109+ return i, err
110+}
111+
86const getSessionByID = `-- name: GetSessionByID :one
112const getSessionByID = `-- name: GetSessionByID :one
87SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
113SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
88FROM sessions
114FROM sessions
internal/db/sql/sessions.sql+6
@@ -28,6 +28,12 @@ SELECT *
28FROM sessions
28FROM sessions
29WHERE id = ? LIMIT 1;
29WHERE id = ? LIMIT 1;
30
30
31+-- name: GetLastSession :one
32+SELECT *
33+FROM sessions
34+ORDER BY updated_at DESC
35+LIMIT 1;
36+
31-- name: ListSessions :many
37-- name: ListSessions :many
32SELECT *
38SELECT *
33FROM sessions
39FROM sessions
internal/event/all.go+20
@@ -61,3 +61,23 @@ func TokensUsed(props ...any) {
61func StatsViewed() {
61func StatsViewed() {
62 send("stats viewed")
62 send("stats viewed")
63}
63}
64+
65+func SessionListed(json bool) {
66+ send("session listed", "json", json)
67+}
68+
69+func SessionShown(json bool) {
70+ send("session shown", "json", json)
71+}
72+
73+func SessionLastShown(json bool) {
74+ send("session last shown", "json", json)
75+}
76+
77+func SessionDeletedCommand(json bool) {
78+ send("session deleted", "json", json)
79+}
80+
81+func SessionRenamed(json bool) {
82+ send("session renamed", "json", json)
83+}
internal/event/event.go+13−3
@@ -17,7 +17,9 @@ const (
17 endpoint = "https://data.charm.land"
17 endpoint = "https://data.charm.land"
18 key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
18 key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
19
19
20− nonInteractiveEventName = "NonInteractive"
20+ nonInteractiveAttrName = "NonInteractive"
21+ continueSessionByIDAttrName = "ContinueSessionByID"
22+ continueLastSessionAttrName = "ContinueLastSession"
21)
23)
22
24
23var (
25var (
@@ -30,11 +32,19 @@ var (
30 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
32 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
31 Set("Version", version.Version).
33 Set("Version", version.Version).
32 Set("GoVersion", runtime.Version()).
34 Set("GoVersion", runtime.Version()).
33− Set(nonInteractiveEventName, false)
35+ Set(nonInteractiveAttrName, false)
34)
36)
35
37
36func SetNonInteractive(nonInteractive bool) {
38func SetNonInteractive(nonInteractive bool) {
37− baseProps = baseProps.Set(nonInteractiveEventName, nonInteractive)
39+ baseProps = baseProps.Set(nonInteractiveAttrName, nonInteractive)
40+}
41+
42+func SetContinueBySessionID(continueBySessionID bool) {
43+ baseProps = baseProps.Set(continueSessionByIDAttrName, continueBySessionID)
44+}
45+
46+func SetContinueLastSession(continueLastSession bool) {
47+ baseProps = baseProps.Set(continueLastSessionAttrName, continueLastSession)
38}
48}
39
49
40func Init() {
50func Init() {
internal/session/session.go+9
@@ -66,6 +66,7 @@ type Service interface {
66 CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
66 CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
67 CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
67 CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
68 Get(ctx context.Context, id string) (Session, error)
68 Get(ctx context.Context, id string) (Session, error)
69+ GetLast(ctx context.Context) (Session, error)
69 List(ctx context.Context) ([]Session, error)
70 List(ctx context.Context) ([]Session, error)
70 Save(ctx context.Context, session Session) (Session, error)
71 Save(ctx context.Context, session Session) (Session, error)
71 UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error
72 UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error
@@ -166,6 +167,14 @@ func (s *service) Get(ctx context.Context, id string) (Session, error) {
166 return s.fromDBItem(dbSession), nil
167 return s.fromDBItem(dbSession), nil
167}
168}
168
169
170+func (s *service) GetLast(ctx context.Context) (Session, error) {
171+ dbSession, err := s.q.GetLastSession(ctx)
172+ if err != nil {
173+ return Session{}, err
174+ }
175+ return s.fromDBItem(dbSession), nil
176+}
177+
169func (s *service) Save(ctx context.Context, session Session) (Session, error) {
178func (s *service) Save(ctx context.Context, session Session) (Session, error) {
170 todosJSON, err := marshalTodos(session.Todos)
179 todosJSON, err := marshalTodos(session.Todos)
171 if err != nil {
180 if err != nil {
internal/ui/chat/agent.go+2−2
@@ -101,7 +101,7 @@ type AgentToolRenderContext struct {
101func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
101func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
102 cappedWidth := cappedMessageWidth(width)
102 cappedWidth := cappedMessageWidth(width)
103 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
103 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
104− return pendingTool(sty, "Agent", opts.Anim)
104+ return pendingTool(sty, "Agent", opts.Anim, opts.Compact)
105 }
105 }
106
106
107 var params agent.AgentParams
107 var params agent.AgentParams
@@ -232,7 +232,7 @@ type agenticFetchParams struct {
232func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
232func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
233 cappedWidth := cappedMessageWidth(width)
233 cappedWidth := cappedMessageWidth(width)
234 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
234 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
235− return pendingTool(sty, "Agentic Fetch", opts.Anim)
235+ return pendingTool(sty, "Agentic Fetch", opts.Anim, opts.Compact)
236 }
236 }
237
237
238 var params agenticFetchParams
238 var params agenticFetchParams
internal/ui/chat/bash.go+3−3
@@ -41,7 +41,7 @@ type BashToolRenderContext struct{}
41func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
41func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
42 cappedWidth := cappedMessageWidth(width)
42 cappedWidth := cappedMessageWidth(width)
43 if opts.IsPending() {
43 if opts.IsPending() {
44− return pendingTool(sty, "Bash", opts.Anim)
44+ return pendingTool(sty, "Bash", opts.Anim, opts.Compact)
45 }
45 }
46
46
47 var params tools.BashParams
47 var params tools.BashParams
@@ -123,7 +123,7 @@ type JobOutputToolRenderContext struct{}
123func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
123func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
124 cappedWidth := cappedMessageWidth(width)
124 cappedWidth := cappedMessageWidth(width)
125 if opts.IsPending() {
125 if opts.IsPending() {
126− return pendingTool(sty, "Job", opts.Anim)
126+ return pendingTool(sty, "Job", opts.Anim, opts.Compact)
127 }
127 }
128
128
129 var params tools.JobOutputParams
129 var params tools.JobOutputParams
@@ -174,7 +174,7 @@ type JobKillToolRenderContext struct{}
174func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
174func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
175 cappedWidth := cappedMessageWidth(width)
175 cappedWidth := cappedMessageWidth(width)
176 if opts.IsPending() {
176 if opts.IsPending() {
177− return pendingTool(sty, "Job", opts.Anim)
177+ return pendingTool(sty, "Job", opts.Anim, opts.Compact)
178 }
178 }
179
179
180 var params tools.JobKillParams
180 var params tools.JobKillParams
internal/ui/chat/diagnostics.go+1−1
@@ -37,7 +37,7 @@ type DiagnosticsToolRenderContext struct{}
37func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
37func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
38 cappedWidth := cappedMessageWidth(width)
38 cappedWidth := cappedMessageWidth(width)
39 if opts.IsPending() {
39 if opts.IsPending() {
40− return pendingTool(sty, "Diagnostics", opts.Anim)
40+ return pendingTool(sty, "Diagnostics", opts.Anim, opts.Compact)
41 }
41 }
42
42
43 var params tools.DiagnosticsParams
43 var params tools.DiagnosticsParams
internal/ui/chat/fetch.go+3−3
@@ -36,7 +36,7 @@ type FetchToolRenderContext struct{}
36func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
36func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
37 cappedWidth := cappedMessageWidth(width)
37 cappedWidth := cappedMessageWidth(width)
38 if opts.IsPending() {
38 if opts.IsPending() {
39− return pendingTool(sty, "Fetch", opts.Anim)
39+ return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
40 }
40 }
41
41
42 var params tools.FetchParams
42 var params tools.FetchParams
@@ -111,7 +111,7 @@ type WebFetchToolRenderContext struct{}
111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
112 cappedWidth := cappedMessageWidth(width)
112 cappedWidth := cappedMessageWidth(width)
113 if opts.IsPending() {
113 if opts.IsPending() {
114− return pendingTool(sty, "Fetch", opts.Anim)
114+ return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
115 }
115 }
116
116
117 var params tools.WebFetchParams
117 var params tools.WebFetchParams
@@ -165,7 +165,7 @@ type WebSearchToolRenderContext struct{}
165func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
165func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
166 cappedWidth := cappedMessageWidth(width)
166 cappedWidth := cappedMessageWidth(width)
167 if opts.IsPending() {
167 if opts.IsPending() {
168− return pendingTool(sty, "Search", opts.Anim)
168+ return pendingTool(sty, "Search", opts.Anim, opts.Compact)
169 }
169 }
170
170
171 var params tools.WebSearchParams
171 var params tools.WebSearchParams
internal/ui/chat/file.go+5−5
@@ -39,7 +39,7 @@ type ViewToolRenderContext struct{}
39func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
39func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
40 cappedWidth := cappedMessageWidth(width)
40 cappedWidth := cappedMessageWidth(width)
41 if opts.IsPending() {
41 if opts.IsPending() {
42− return pendingTool(sty, "View", opts.Anim)
42+ return pendingTool(sty, "View", opts.Anim, opts.Compact)
43 }
43 }
44
44
45 var params tools.ViewParams
45 var params tools.ViewParams
@@ -125,7 +125,7 @@ type WriteToolRenderContext struct{}
125func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
125func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
126 cappedWidth := cappedMessageWidth(width)
126 cappedWidth := cappedMessageWidth(width)
127 if opts.IsPending() {
127 if opts.IsPending() {
128− return pendingTool(sty, "Write", opts.Anim)
128+ return pendingTool(sty, "Write", opts.Anim, opts.Compact)
129 }
129 }
130
130
131 var params tools.WriteParams
131 var params tools.WriteParams
@@ -180,7 +180,7 @@ type EditToolRenderContext struct{}
180func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
180func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
181 // Edit tool uses full width for diffs.
181 // Edit tool uses full width for diffs.
182 if opts.IsPending() {
182 if opts.IsPending() {
183− return pendingTool(sty, "Edit", opts.Anim)
183+ return pendingTool(sty, "Edit", opts.Anim, opts.Compact)
184 }
184 }
185
185
186 var params tools.EditParams
186 var params tools.EditParams
@@ -243,7 +243,7 @@ type MultiEditToolRenderContext struct{}
243func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
243func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
244 // MultiEdit tool uses full width for diffs.
244 // MultiEdit tool uses full width for diffs.
245 if opts.IsPending() {
245 if opts.IsPending() {
246− return pendingTool(sty, "Multi-Edit", opts.Anim)
246+ return pendingTool(sty, "Multi-Edit", opts.Anim, opts.Compact)
247 }
247 }
248
248
249 var params tools.MultiEditParams
249 var params tools.MultiEditParams
@@ -311,7 +311,7 @@ type DownloadToolRenderContext struct{}
311func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
311func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
312 cappedWidth := cappedMessageWidth(width)
312 cappedWidth := cappedMessageWidth(width)
313 if opts.IsPending() {
313 if opts.IsPending() {
314− return pendingTool(sty, "Download", opts.Anim)
314+ return pendingTool(sty, "Download", opts.Anim, opts.Compact)
315 }
315 }
316
316
317 var params tools.DownloadParams
317 var params tools.DownloadParams
internal/ui/chat/generic.go+1−1
@@ -35,7 +35,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
35 name := genericPrettyName(opts.ToolCall.Name)
35 name := genericPrettyName(opts.ToolCall.Name)
36
36
37 if opts.IsPending() {
37 if opts.IsPending() {
38− return pendingTool(sty, name, opts.Anim)
38+ return pendingTool(sty, name, opts.Anim, opts.Compact)
39 }
39 }
40
40
41 var params map[string]any
41 var params map[string]any
internal/ui/chat/lsp_restart.go+1−1
@@ -32,7 +32,7 @@ type LSPRestartToolRenderContext struct{}
32func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
32func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
33 cappedWidth := cappedMessageWidth(width)
33 cappedWidth := cappedMessageWidth(width)
34 if opts.IsPending() {
34 if opts.IsPending() {
35− return pendingTool(sty, "Restart LSP", opts.Anim)
35+ return pendingTool(sty, "Restart LSP", opts.Anim, opts.Compact)
36 }
36 }
37
37
38 var params tools.LSPRestartParams
38 var params tools.LSPRestartParams
internal/ui/chat/mcp.go+1−1
@@ -46,7 +46,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
46 name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
46 name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
47
47
48 if opts.IsPending() {
48 if opts.IsPending() {
49− return pendingTool(sty, name, opts.Anim)
49+ return pendingTool(sty, name, opts.Anim, opts.Compact)
50 }
50 }
51
51
52 var params map[string]any
52 var params map[string]any
internal/ui/chat/references.go+1−1
@@ -33,7 +33,7 @@ type ReferencesToolRenderContext struct{}
33func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
33func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
34 cappedWidth := cappedMessageWidth(width)
34 cappedWidth := cappedMessageWidth(width)
35 if opts.IsPending() {
35 if opts.IsPending() {
36− return pendingTool(sty, "Find References", opts.Anim)
36+ return pendingTool(sty, "Find References", opts.Anim, opts.Compact)
37 }
37 }
38
38
39 var params tools.ReferencesParams
39 var params tools.ReferencesParams
internal/ui/chat/search.go+4−4
@@ -37,7 +37,7 @@ type GlobToolRenderContext struct{}
37func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
37func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
38 cappedWidth := cappedMessageWidth(width)
38 cappedWidth := cappedMessageWidth(width)
39 if opts.IsPending() {
39 if opts.IsPending() {
40− return pendingTool(sty, "Glob", opts.Anim)
40+ return pendingTool(sty, "Glob", opts.Anim, opts.Compact)
41 }
41 }
42
42
43 var params tools.GlobParams
43 var params tools.GlobParams
@@ -96,7 +96,7 @@ type GrepToolRenderContext struct{}
96func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
96func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
97 cappedWidth := cappedMessageWidth(width)
97 cappedWidth := cappedMessageWidth(width)
98 if opts.IsPending() {
98 if opts.IsPending() {
99− return pendingTool(sty, "Grep", opts.Anim)
99+ return pendingTool(sty, "Grep", opts.Anim, opts.Compact)
100 }
100 }
101
101
102 var params tools.GrepParams
102 var params tools.GrepParams
@@ -161,7 +161,7 @@ type LSToolRenderContext struct{}
161func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
161func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
162 cappedWidth := cappedMessageWidth(width)
162 cappedWidth := cappedMessageWidth(width)
163 if opts.IsPending() {
163 if opts.IsPending() {
164− return pendingTool(sty, "List", opts.Anim)
164+ return pendingTool(sty, "List", opts.Anim, opts.Compact)
165 }
165 }
166
166
167 var params tools.LSParams
167 var params tools.LSParams
@@ -221,7 +221,7 @@ type SourcegraphToolRenderContext struct{}
221func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
221func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
222 cappedWidth := cappedMessageWidth(width)
222 cappedWidth := cappedMessageWidth(width)
223 if opts.IsPending() {
223 if opts.IsPending() {
224− return pendingTool(sty, "Sourcegraph", opts.Anim)
224+ return pendingTool(sty, "Sourcegraph", opts.Anim, opts.Compact)
225 }
225 }
226
226
227 var params tools.SourcegraphParams
227 var params tools.SourcegraphParams
internal/ui/chat/todos.go+1−1
@@ -41,7 +41,7 @@ type TodosToolRenderContext struct{}
41func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
41func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
42 cappedWidth := cappedMessageWidth(width)
42 cappedWidth := cappedMessageWidth(width)
43 if opts.IsPending() {
43 if opts.IsPending() {
44− return pendingTool(sty, "To-Do", opts.Anim)
44+ return pendingTool(sty, "To-Do", opts.Anim, opts.Compact)
45 }
45 }
46
46
47 var params tools.TodosParams
47 var params tools.TodosParams
internal/ui/chat/tools.go+6−2
@@ -424,9 +424,13 @@ func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
424}
424}
425
425
426// pendingTool renders a tool that is still in progress with an animation.
426// pendingTool renders a tool that is still in progress with an animation.
427−func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
427+func pendingTool(sty *styles.Styles, name string, anim *anim.Anim, nested bool) string {
428 icon := sty.Tool.IconPending.Render()
428 icon := sty.Tool.IconPending.Render()
429− toolName := sty.Tool.NameNormal.Render(name)
429+ nameStyle := sty.Tool.NameNormal
430+ if nested {
431+ nameStyle = sty.Tool.NameNested
432+ }
433+ toolName := nameStyle.Render(name)
430
434
431 var animView string
435 var animView string
432 if anim != nil {
436 if anim != nil {
internal/ui/dialog/models_list.go+3−1
@@ -216,8 +216,10 @@ func (f *ModelsList) VisibleItems() []list.Item {
216 }
216 }
217
217
218 matches := fuzzy.Find(query, names)
218 matches := fuzzy.Find(query, names)
219+
220+ // Sort by original index to preserve order within the group
219 sort.SliceStable(matches, func(i, j int) bool {
221 sort.SliceStable(matches, func(i, j int) bool {
220− return matches[i].Score > matches[j].Score
222+ return matches[i].Index < matches[j].Index
221 })
223 })
222
224
223 for _, match := range matches {
225 for _, match := range matches {
internal/ui/styles/styles.go+77−81
@@ -254,18 +254,18 @@ type Styles struct {
254 // Tool - styles for tool call rendering
254 // Tool - styles for tool call rendering
255 Tool struct {
255 Tool struct {
256 // Icon styles with tool status
256 // Icon styles with tool status
257− IconPending lipgloss.Style // Pending operation icon
257+ IconPending lipgloss.Style
258− IconSuccess lipgloss.Style // Successful operation icon
258+ IconSuccess lipgloss.Style
259− IconError lipgloss.Style // Error operation icon
259+ IconError lipgloss.Style
260− IconCancelled lipgloss.Style // Cancelled operation icon
260+ IconCancelled lipgloss.Style
261
261
262 // Tool name styles
262 // Tool name styles
263− NameNormal lipgloss.Style // Normal tool name
263+ NameNormal lipgloss.Style // Top-level tool name
264− NameNested lipgloss.Style // Nested tool name
264+ NameNested lipgloss.Style // Nested child tool name (inside Agent/Agentic Fetch)
265
265
266 // Parameter list styles
266 // Parameter list styles
267− ParamMain lipgloss.Style // Main parameter
267+ ParamMain lipgloss.Style
268− ParamKey lipgloss.Style // Parameter keys
268+ ParamKey lipgloss.Style
269
269
270 // Content rendering styles
270 // Content rendering styles
271 ContentLine lipgloss.Style // Individual content line with background and width
271 ContentLine lipgloss.Style // Individual content line with background and width
@@ -632,14 +632,14 @@ func DefaultStyles() Styles {
632 StylePrimitive: ansi.StylePrimitive{
632 StylePrimitive: ansi.StylePrimitive{
633 // BlockPrefix: "\n",
633 // BlockPrefix: "\n",
634 // BlockSuffix: "\n",
634 // BlockSuffix: "\n",
635− Color: stringPtr(charmtone.Smoke.Hex()),
635+ Color: new(charmtone.Smoke.Hex()),
636 },
636 },
637− // Margin: uintPtr(defaultMargin),
637+ // Margin: new(uint(defaultMargin)),
638 },
638 },
639 BlockQuote: ansi.StyleBlock{
639 BlockQuote: ansi.StyleBlock{
640 StylePrimitive: ansi.StylePrimitive{},
640 StylePrimitive: ansi.StylePrimitive{},
641− Indent: uintPtr(1),
641+ Indent: new(uint(1)),
642− IndentToken: stringPtr("│ "),
642+ IndentToken: new("│ "),
643 },
643 },
644 List: ansi.StyleList{
644 List: ansi.StyleList{
645 LevelIndent: defaultListIndent,
645 LevelIndent: defaultListIndent,
@@ -647,17 +647,17 @@ func DefaultStyles() Styles {
647 Heading: ansi.StyleBlock{
647 Heading: ansi.StyleBlock{
648 StylePrimitive: ansi.StylePrimitive{
648 StylePrimitive: ansi.StylePrimitive{
649 BlockSuffix: "\n",
649 BlockSuffix: "\n",
650− Color: stringPtr(charmtone.Malibu.Hex()),
650+ Color: new(charmtone.Malibu.Hex()),
651− Bold: boolPtr(true),
651+ Bold: new(true),
652 },
652 },
653 },
653 },
654 H1: ansi.StyleBlock{
654 H1: ansi.StyleBlock{
655 StylePrimitive: ansi.StylePrimitive{
655 StylePrimitive: ansi.StylePrimitive{
656 Prefix: " ",
656 Prefix: " ",
657 Suffix: " ",
657 Suffix: " ",
658− Color: stringPtr(charmtone.Zest.Hex()),
658+ Color: new(charmtone.Zest.Hex()),
659− BackgroundColor: stringPtr(charmtone.Charple.Hex()),
659+ BackgroundColor: new(charmtone.Charple.Hex()),
660− Bold: boolPtr(true),
660+ Bold: new(true),
661 },
661 },
662 },
662 },
663 H2: ansi.StyleBlock{
663 H2: ansi.StyleBlock{
@@ -683,21 +683,21 @@ func DefaultStyles() Styles {
683 H6: ansi.StyleBlock{
683 H6: ansi.StyleBlock{
684 StylePrimitive: ansi.StylePrimitive{
684 StylePrimitive: ansi.StylePrimitive{
685 Prefix: "###### ",
685 Prefix: "###### ",
686− Color: stringPtr(charmtone.Guac.Hex()),
686+ Color: new(charmtone.Guac.Hex()),
687− Bold: boolPtr(false),
687+ Bold: new(false),
688 },
688 },
689 },
689 },
690 Strikethrough: ansi.StylePrimitive{
690 Strikethrough: ansi.StylePrimitive{
691− CrossedOut: boolPtr(true),
691+ CrossedOut: new(true),
692 },
692 },
693 Emph: ansi.StylePrimitive{
693 Emph: ansi.StylePrimitive{
694− Italic: boolPtr(true),
694+ Italic: new(true),
695 },
695 },
696 Strong: ansi.StylePrimitive{
696 Strong: ansi.StylePrimitive{
697− Bold: boolPtr(true),
697+ Bold: new(true),
698 },
698 },
699 HorizontalRule: ansi.StylePrimitive{
699 HorizontalRule: ansi.StylePrimitive{
700− Color: stringPtr(charmtone.Charcoal.Hex()),
700+ Color: new(charmtone.Charcoal.Hex()),
701 Format: "\n--------\n",
701 Format: "\n--------\n",
702 },
702 },
703 Item: ansi.StylePrimitive{
703 Item: ansi.StylePrimitive{
@@ -712,117 +712,117 @@ func DefaultStyles() Styles {
712 Unticked: "[ ] ",
712 Unticked: "[ ] ",
713 },
713 },
714 Link: ansi.StylePrimitive{
714 Link: ansi.StylePrimitive{
715− Color: stringPtr(charmtone.Zinc.Hex()),
715+ Color: new(charmtone.Zinc.Hex()),
716− Underline: boolPtr(true),
716+ Underline: new(true),
717 },
717 },
718 LinkText: ansi.StylePrimitive{
718 LinkText: ansi.StylePrimitive{
719− Color: stringPtr(charmtone.Guac.Hex()),
719+ Color: new(charmtone.Guac.Hex()),
720− Bold: boolPtr(true),
720+ Bold: new(true),
721 },
721 },
722 Image: ansi.StylePrimitive{
722 Image: ansi.StylePrimitive{
723− Color: stringPtr(charmtone.Cheeky.Hex()),
723+ Color: new(charmtone.Cheeky.Hex()),
724− Underline: boolPtr(true),
724+ Underline: new(true),
725 },
725 },
726 ImageText: ansi.StylePrimitive{
726 ImageText: ansi.StylePrimitive{
727− Color: stringPtr(charmtone.Squid.Hex()),
727+ Color: new(charmtone.Squid.Hex()),
728 Format: "Image: {{.text}} →",
728 Format: "Image: {{.text}} →",
729 },
729 },
730 Code: ansi.StyleBlock{
730 Code: ansi.StyleBlock{
731 StylePrimitive: ansi.StylePrimitive{
731 StylePrimitive: ansi.StylePrimitive{
732 Prefix: " ",
732 Prefix: " ",
733 Suffix: " ",
733 Suffix: " ",
734− Color: stringPtr(charmtone.Coral.Hex()),
734+ Color: new(charmtone.Coral.Hex()),
735− BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
735+ BackgroundColor: new(charmtone.Charcoal.Hex()),
736 },
736 },
737 },
737 },
738 CodeBlock: ansi.StyleCodeBlock{
738 CodeBlock: ansi.StyleCodeBlock{
739 StyleBlock: ansi.StyleBlock{
739 StyleBlock: ansi.StyleBlock{
740 StylePrimitive: ansi.StylePrimitive{
740 StylePrimitive: ansi.StylePrimitive{
741− Color: stringPtr(charmtone.Charcoal.Hex()),
741+ Color: new(charmtone.Charcoal.Hex()),
742 },
742 },
743− Margin: uintPtr(defaultMargin),
743+ Margin: new(uint(defaultMargin)),
744 },
744 },
745 Chroma: &ansi.Chroma{
745 Chroma: &ansi.Chroma{
746 Text: ansi.StylePrimitive{
746 Text: ansi.StylePrimitive{
747− Color: stringPtr(charmtone.Smoke.Hex()),
747+ Color: new(charmtone.Smoke.Hex()),
748 },
748 },
749 Error: ansi.StylePrimitive{
749 Error: ansi.StylePrimitive{
750− Color: stringPtr(charmtone.Butter.Hex()),
750+ Color: new(charmtone.Butter.Hex()),
751− BackgroundColor: stringPtr(charmtone.Sriracha.Hex()),
751+ BackgroundColor: new(charmtone.Sriracha.Hex()),
752 },
752 },
753 Comment: ansi.StylePrimitive{
753 Comment: ansi.StylePrimitive{
754− Color: stringPtr(charmtone.Oyster.Hex()),
754+ Color: new(charmtone.Oyster.Hex()),
755 },
755 },
756 CommentPreproc: ansi.StylePrimitive{
756 CommentPreproc: ansi.StylePrimitive{
757− Color: stringPtr(charmtone.Bengal.Hex()),
757+ Color: new(charmtone.Bengal.Hex()),
758 },
758 },
759 Keyword: ansi.StylePrimitive{
759 Keyword: ansi.StylePrimitive{
760− Color: stringPtr(charmtone.Malibu.Hex()),
760+ Color: new(charmtone.Malibu.Hex()),
761 },
761 },
762 KeywordReserved: ansi.StylePrimitive{
762 KeywordReserved: ansi.StylePrimitive{
763− Color: stringPtr(charmtone.Pony.Hex()),
763+ Color: new(charmtone.Pony.Hex()),
764 },
764 },
765 KeywordNamespace: ansi.StylePrimitive{
765 KeywordNamespace: ansi.StylePrimitive{
766− Color: stringPtr(charmtone.Pony.Hex()),
766+ Color: new(charmtone.Pony.Hex()),
767 },
767 },
768 KeywordType: ansi.StylePrimitive{
768 KeywordType: ansi.StylePrimitive{
769− Color: stringPtr(charmtone.Guppy.Hex()),
769+ Color: new(charmtone.Guppy.Hex()),
770 },
770 },
771 Operator: ansi.StylePrimitive{
771 Operator: ansi.StylePrimitive{
772− Color: stringPtr(charmtone.Salmon.Hex()),
772+ Color: new(charmtone.Salmon.Hex()),
773 },
773 },
774 Punctuation: ansi.StylePrimitive{
774 Punctuation: ansi.StylePrimitive{
775− Color: stringPtr(charmtone.Zest.Hex()),
775+ Color: new(charmtone.Zest.Hex()),
776 },
776 },
777 Name: ansi.StylePrimitive{
777 Name: ansi.StylePrimitive{
778− Color: stringPtr(charmtone.Smoke.Hex()),
778+ Color: new(charmtone.Smoke.Hex()),
779 },
779 },
780 NameBuiltin: ansi.StylePrimitive{
780 NameBuiltin: ansi.StylePrimitive{
781− Color: stringPtr(charmtone.Cheeky.Hex()),
781+ Color: new(charmtone.Cheeky.Hex()),
782 },
782 },
783 NameTag: ansi.StylePrimitive{
783 NameTag: ansi.StylePrimitive{
784− Color: stringPtr(charmtone.Mauve.Hex()),
784+ Color: new(charmtone.Mauve.Hex()),
785 },
785 },
786 NameAttribute: ansi.StylePrimitive{
786 NameAttribute: ansi.StylePrimitive{
787− Color: stringPtr(charmtone.Hazy.Hex()),
787+ Color: new(charmtone.Hazy.Hex()),
788 },
788 },
789 NameClass: ansi.StylePrimitive{
789 NameClass: ansi.StylePrimitive{
790− Color: stringPtr(charmtone.Salt.Hex()),
790+ Color: new(charmtone.Salt.Hex()),
791− Underline: boolPtr(true),
791+ Underline: new(true),
792− Bold: boolPtr(true),
792+ Bold: new(true),
793 },
793 },
794 NameDecorator: ansi.StylePrimitive{
794 NameDecorator: ansi.StylePrimitive{
795− Color: stringPtr(charmtone.Citron.Hex()),
795+ Color: new(charmtone.Citron.Hex()),
796 },
796 },
797 NameFunction: ansi.StylePrimitive{
797 NameFunction: ansi.StylePrimitive{
798− Color: stringPtr(charmtone.Guac.Hex()),
798+ Color: new(charmtone.Guac.Hex()),
799 },
799 },
800 LiteralNumber: ansi.StylePrimitive{
800 LiteralNumber: ansi.StylePrimitive{
801− Color: stringPtr(charmtone.Julep.Hex()),
801+ Color: new(charmtone.Julep.Hex()),
802 },
802 },
803 LiteralString: ansi.StylePrimitive{
803 LiteralString: ansi.StylePrimitive{
804− Color: stringPtr(charmtone.Cumin.Hex()),
804+ Color: new(charmtone.Cumin.Hex()),
805 },
805 },
806 LiteralStringEscape: ansi.StylePrimitive{
806 LiteralStringEscape: ansi.StylePrimitive{
807− Color: stringPtr(charmtone.Bok.Hex()),
807+ Color: new(charmtone.Bok.Hex()),
808 },
808 },
809 GenericDeleted: ansi.StylePrimitive{
809 GenericDeleted: ansi.StylePrimitive{
810− Color: stringPtr(charmtone.Coral.Hex()),
810+ Color: new(charmtone.Coral.Hex()),
811 },
811 },
812 GenericEmph: ansi.StylePrimitive{
812 GenericEmph: ansi.StylePrimitive{
813− Italic: boolPtr(true),
813+ Italic: new(true),
814 },
814 },
815 GenericInserted: ansi.StylePrimitive{
815 GenericInserted: ansi.StylePrimitive{
816− Color: stringPtr(charmtone.Guac.Hex()),
816+ Color: new(charmtone.Guac.Hex()),
817 },
817 },
818 GenericStrong: ansi.StylePrimitive{
818 GenericStrong: ansi.StylePrimitive{
819− Bold: boolPtr(true),
819+ Bold: new(true),
820 },
820 },
821 GenericSubheading: ansi.StylePrimitive{
821 GenericSubheading: ansi.StylePrimitive{
822− Color: stringPtr(charmtone.Squid.Hex()),
822+ Color: new(charmtone.Squid.Hex()),
823 },
823 },
824 Background: ansi.StylePrimitive{
824 Background: ansi.StylePrimitive{
825− BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
825+ BackgroundColor: new(charmtone.Charcoal.Hex()),
826 },
826 },
827 },
827 },
828 },
828 },
@@ -837,8 +837,8 @@ func DefaultStyles() Styles {
837 }
837 }
838
838
839 // PlainMarkdown style - muted colors on subtle background for thinking content.
839 // PlainMarkdown style - muted colors on subtle background for thinking content.
840− plainBg := stringPtr(bgBaseLighter.Hex())
840+ plainBg := new(bgBaseLighter.Hex())
841− plainFg := stringPtr(fgMuted.Hex())
841+ plainFg := new(fgMuted.Hex())
842 s.PlainMarkdown = ansi.StyleConfig{
842 s.PlainMarkdown = ansi.StyleConfig{
843 Document: ansi.StyleBlock{
843 Document: ansi.StyleBlock{
844 StylePrimitive: ansi.StylePrimitive{
844 StylePrimitive: ansi.StylePrimitive{
@@ -851,8 +851,8 @@ func DefaultStyles() Styles {
851 Color: plainFg,
851 Color: plainFg,
852 BackgroundColor: plainBg,
852 BackgroundColor: plainBg,
853 },
853 },
854− Indent: uintPtr(1),
854+ Indent: new(uint(1)),
855− IndentToken: stringPtr("│ "),
855+ IndentToken: new("│ "),
856 },
856 },
857 List: ansi.StyleList{
857 List: ansi.StyleList{
858 LevelIndent: defaultListIndent,
858 LevelIndent: defaultListIndent,
@@ -860,7 +860,7 @@ func DefaultStyles() Styles {
860 Heading: ansi.StyleBlock{
860 Heading: ansi.StyleBlock{
861 StylePrimitive: ansi.StylePrimitive{
861 StylePrimitive: ansi.StylePrimitive{
862 BlockSuffix: "\n",
862 BlockSuffix: "\n",
863− Bold: boolPtr(true),
863+ Bold: new(true),
864 Color: plainFg,
864 Color: plainFg,
865 BackgroundColor: plainBg,
865 BackgroundColor: plainBg,
866 },
866 },
@@ -869,7 +869,7 @@ func DefaultStyles() Styles {
869 StylePrimitive: ansi.StylePrimitive{
869 StylePrimitive: ansi.StylePrimitive{
870 Prefix: " ",
870 Prefix: " ",
871 Suffix: " ",
871 Suffix: " ",
872− Bold: boolPtr(true),
872+ Bold: new(true),
873 Color: plainFg,
873 Color: plainFg,
874 BackgroundColor: plainBg,
874 BackgroundColor: plainBg,
875 },
875 },
@@ -910,17 +910,17 @@ func DefaultStyles() Styles {
910 },
910 },
911 },
911 },
912 Strikethrough: ansi.StylePrimitive{
912 Strikethrough: ansi.StylePrimitive{
913− CrossedOut: boolPtr(true),
913+ CrossedOut: new(true),
914 Color: plainFg,
914 Color: plainFg,
915 BackgroundColor: plainBg,
915 BackgroundColor: plainBg,
916 },
916 },
917 Emph: ansi.StylePrimitive{
917 Emph: ansi.StylePrimitive{
918− Italic: boolPtr(true),
918+ Italic: new(true),
919 Color: plainFg,
919 Color: plainFg,
920 BackgroundColor: plainBg,
920 BackgroundColor: plainBg,
921 },
921 },
922 Strong: ansi.StylePrimitive{
922 Strong: ansi.StylePrimitive{
923− Bold: boolPtr(true),
923+ Bold: new(true),
924 Color: plainFg,
924 Color: plainFg,
925 BackgroundColor: plainBg,
925 BackgroundColor: plainBg,
926 },
926 },
@@ -948,17 +948,17 @@ func DefaultStyles() Styles {
948 Unticked: "[ ] ",
948 Unticked: "[ ] ",
949 },
949 },
950 Link: ansi.StylePrimitive{
950 Link: ansi.StylePrimitive{
951− Underline: boolPtr(true),
951+ Underline: new(true),
952 Color: plainFg,
952 Color: plainFg,
953 BackgroundColor: plainBg,
953 BackgroundColor: plainBg,
954 },
954 },
955 LinkText: ansi.StylePrimitive{
955 LinkText: ansi.StylePrimitive{
956− Bold: boolPtr(true),
956+ Bold: new(true),
957 Color: plainFg,
957 Color: plainFg,
958 BackgroundColor: plainBg,
958 BackgroundColor: plainBg,
959 },
959 },
960 Image: ansi.StylePrimitive{
960 Image: ansi.StylePrimitive{
961− Underline: boolPtr(true),
961+ Underline: new(true),
962 Color: plainFg,
962 Color: plainFg,
963 BackgroundColor: plainBg,
963 BackgroundColor: plainBg,
964 },
964 },
@@ -981,7 +981,7 @@ func DefaultStyles() Styles {
981 Color: plainFg,
981 Color: plainFg,
982 BackgroundColor: plainBg,
982 BackgroundColor: plainBg,
983 },
983 },
984− Margin: uintPtr(defaultMargin),
984+ Margin: new(uint(defaultMargin)),
985 },
985 },
986 },
986 },
987 Table: ansi.StyleTable{
987 Table: ansi.StyleTable{
@@ -1119,7 +1119,7 @@ func DefaultStyles() Styles {
1119 s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
1119 s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
1120
1120
1121 s.Tool.NameNormal = base.Foreground(blue)
1121 s.Tool.NameNormal = base.Foreground(blue)
1122− s.Tool.NameNested = base.Foreground(fgHalfMuted)
1122+ s.Tool.NameNested = base.Foreground(blue)
1123
1123
1124 s.Tool.ParamMain = s.Subtle
1124 s.Tool.ParamMain = s.Subtle
1125 s.Tool.ParamKey = s.Subtle
1125 s.Tool.ParamKey = s.Subtle
@@ -1368,10 +1368,6 @@ func DefaultStyles() Styles {
1368 return s
1368 return s
1369}
1369}
1370
1370
1371−// Helper functions for style pointers
1372−func boolPtr(b bool) *bool { return &b }
1373−func stringPtr(s string) *string { return &s }
1374−func uintPtr(u uint) *uint { return &u }
1375func chromaStyle(style ansi.StylePrimitive) string {
1371func chromaStyle(style ansi.StylePrimitive) string {
1376 var s strings.Builder
1372 var s strings.Builder
1377
1373