on main help sign in
Merge remote-tracking branch 'origin/main' into server-client-2
commitee13558119e539ed205d52aaa9d107e0b7a208a5 authorAyman Bagabas <ayman.bagabas@gmail.com> dateTue 17 Mar 2026, 01:01 (7 months ago) parente8d2835a533d78
30 files changed+551−130
.github/workflows/security.yml+6−6 Taskfile.yaml+5 go.mod+1−1 go.sum+2−2 internal/app/app.go+39−3 internal/app/resolve_session_test.go+174 internal/cmd/run.go+108−8 internal/cmd/session.go+16 internal/config/config.go+6 internal/db/db.go+10 internal/db/querier.go+1 internal/db/sessions.sql.go+26 internal/db/sql/sessions.sql+6 internal/event/all.go+20 internal/event/event.go+13−3 internal/session/session.go+9 internal/ui/chat/agent.go+2−2 internal/ui/chat/bash.go+3−3 internal/ui/chat/diagnostics.go+1−1 internal/ui/chat/fetch.go+3−3 internal/ui/chat/file.go+5−5 internal/ui/chat/generic.go+1−1 internal/ui/chat/lsp_restart.go+1−1 internal/ui/chat/mcp.go+1−1 internal/ui/chat/references.go+1−1 internal/ui/chat/search.go+4−4 internal/ui/chat/todos.go+1−1 internal/ui/chat/tools.go+6−2 internal/ui/dialog/models_list.go+3−1 internal/ui/styles/styles.go+77−81
.github/workflows/security.yml+6−6
@@ -30,11 +30,11 @@ jobs:
3030 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3131 with:
3232 persist-credentials: false
33− - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
33+ - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
3434 with:
3535 languages: ${{ matrix.language }}
36− - uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
37− - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
36+ - uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
37+ - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
3838
3939 grype:
4040 runs-on: ubuntu-latest
@@ -52,7 +52,7 @@ jobs:
5252 path: "."
5353 fail-build: true
5454 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
5656 with:
5757 sarif_file: ${{ steps.scan.outputs.sarif }}
5858
@@ -73,7 +73,7 @@ jobs:
7373 - name: Run govulncheck
7474 run: |
7575 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
7777 with:
7878 sarif_file: results.sarif
7979
@@ -86,7 +86,7 @@ jobs:
8686 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
8787 with:
8888 persist-credentials: false
89− - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
89+ - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
9090 with:
9191 fail-on-severity: critical
9292 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:
184184 - go get charm.land/fantasy
185185 - go get charm.land/catwalk
186186 - 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
55require (
66 charm.land/bubbles/v2 v2.0.0
77 charm.land/bubbletea/v2 v2.0.2
8− charm.land/catwalk v0.30.0
8+ charm.land/catwalk v0.30.3
99 charm.land/fang/v2 v2.0.1
1010 charm.land/fantasy v0.12.3
1111 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=
22charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
33charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
44charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
5−charm.land/catwalk v0.30.0 h1:CgWN3hM0HYYXlxMFsd1mfpBliqvx9gH3LolVi1VfMSU=
6−charm.land/catwalk v0.30.0/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
5+charm.land/catwalk v0.30.3 h1:eCRwVoi1znrNGYiPZoBIbWt8+Q4kDhT3zziqnPO3s2Y=
6+charm.land/catwalk v0.30.3/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
77charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
88charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
99charm.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] {
171171 return app.agentNotifications
172172}
173173
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+
174205// RunNonInteractive runs the application in non-interactive mode with the
175206// 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 {
177208 slog.Info("Running in non-interactive mode")
178209
179210 ctx, cancel := context.WithCancel(ctx)
@@ -241,11 +272,16 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
241272
242273 defer stopSpinner()
243274
244− sess, err := app.Sessions.Create(ctx, agent.DefaultSessionName)
275+ sess, err := app.resolveSession(ctx, continueSessionID, useLast)
245276 if err != nil {
246277 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
247278 }
248− slog.Info("Created session for non-interactive run", "session_id", sess.ID)
279+
280+ if continueSessionID != "" || useLast {
281+ slog.Info("Continuing session for non-interactive run", "session_id", sess.ID)
282+ } else {
283+ slog.Info("Created session for non-interactive run", "session_id", sess.ID)
284+ }
249285
250286 // Automatically approve all permission requests for this non-interactive
251287 // 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 (
1717 "github.com/charmbracelet/crush/internal/format"
1818 "github.com/charmbracelet/crush/internal/proto"
1919 "github.com/charmbracelet/crush/internal/pubsub"
20+ "github.com/charmbracelet/crush/internal/session"
2021 "github.com/charmbracelet/crush/internal/ui/anim"
2122 "github.com/charmbracelet/crush/internal/ui/styles"
2223 "github.com/charmbracelet/x/ansi"
@@ -48,12 +49,23 @@ crush run --quiet "Generate a README for this project"
4849
4950# Run in verbose mode (show logs)
5051crush 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+
5159 `,
5260 RunE: func(cmd *cobra.Command, args []string) error {
53− quiet, _ := cmd.Flags().GetBool("quiet")
54− verbose, _ := cmd.Flags().GetBool("verbose")
55− largeModel, _ := cmd.Flags().GetString("model")
56− smallModel, _ := cmd.Flags().GetString("small-model")
61+ var (
62+ quiet, _ = cmd.Flags().GetBool("quiet")
63+ verbose, _ = cmd.Flags().GetBool("verbose")
64+ largeModel, _ = cmd.Flags().GetString("model")
65+ smallModel, _ = cmd.Flags().GetString("small-model")
66+ sessionID, _ = cmd.Flags().GetString("session")
67+ useLast, _ = cmd.Flags().GetBool("continue")
68+ )
5769
5870 // Cancel on SIGINT or SIGTERM.
5971 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
@@ -65,6 +77,14 @@ crush run --verbose "Generate a README for this project"
6577 }
6678 defer cleanup()
6779
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+
6888 if !ws.Config.IsConfigured() {
6989 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
7090 }
@@ -88,7 +108,14 @@ crush run --verbose "Generate a README for this project"
88108 event.SetNonInteractive(true)
89109 event.AppInitialized()
90110
91− return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose)
111+ switch {
112+ case sessionID != "":
113+ event.SetContinueBySessionID(true)
114+ case useLast:
115+ event.SetContinueLastSession(true)
116+ }
117+
118+ return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose, sessionID, useLast)
92119 },
93120}
94121
@@ -97,6 +124,9 @@ func init() {
97124 runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
98125 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
99126 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")
100130}
101131
102132// runNonInteractive executes the agent via the server and streams output
@@ -107,6 +137,8 @@ func runNonInteractive(
107137 ws *proto.Workspace,
108138 prompt, largeModel, smallModel string,
109139 hideSpinner bool,
140+ continueSessionID string,
141+ useLast bool,
110142) error {
111143 slog.Info("Running in non-interactive mode")
112144
@@ -172,11 +204,15 @@ func runNonInteractive(
172204
173205 defer stopSpinner()
174206
175− sess, err := c.CreateSession(ctx, ws.ID, "non-interactive")
207+ sess, err := resolveSession(ctx, c, ws.ID, continueSessionID, useLast)
176208 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)
178215 }
179− slog.Info("Created session for non-interactive run", "session_id", sess.ID)
180216
181217 events, err := c.SubscribeEvents(ctx, ws.ID)
182218 if err != nil {
@@ -407,3 +443,67 @@ func validateModelMatches(matches []modelMatch, modelID, label string) (modelMat
407443 }
408444 return matches[0], nil
409445}
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 (
1717 "github.com/charmbracelet/colorprofile"
1818 "github.com/charmbracelet/crush/internal/config"
1919 "github.com/charmbracelet/crush/internal/db"
20+ "github.com/charmbracelet/crush/internal/event"
2021 "github.com/charmbracelet/crush/internal/message"
2122 "github.com/charmbracelet/crush/internal/session"
2223 "github.com/charmbracelet/crush/internal/ui/chat"
@@ -126,6 +127,9 @@ func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func()
126127}
127128
128129func runSessionList(cmd *cobra.Command, _ []string) error {
130+ event.SetNonInteractive(true)
131+ event.SessionListed(sessionListJSON)
132+
129133 ctx, svc, cleanup, err := sessionSetup(cmd)
130134 if err != nil {
131135 return err
@@ -249,6 +253,9 @@ func resolveSessionID(ctx context.Context, svc session.Service, id string) (sess
249253}
250254
251255func runSessionShow(cmd *cobra.Command, args []string) error {
256+ event.SetNonInteractive(true)
257+ event.SessionShown(sessionShowJSON)
258+
252259 ctx, svc, cleanup, err := sessionSetup(cmd)
253260 if err != nil {
254261 return err
@@ -273,6 +280,9 @@ func runSessionShow(cmd *cobra.Command, args []string) error {
273280}
274281
275282func runSessionDelete(cmd *cobra.Command, args []string) error {
283+ event.SetNonInteractive(true)
284+ event.SessionDeletedCommand(sessionDeleteJSON)
285+
276286 ctx, svc, cleanup, err := sessionSetup(cmd)
277287 if err != nil {
278288 return err
@@ -305,6 +315,9 @@ func runSessionDelete(cmd *cobra.Command, args []string) error {
305315}
306316
307317func runSessionRename(cmd *cobra.Command, args []string) error {
318+ event.SetNonInteractive(true)
319+ event.SessionRenamed(sessionRenameJSON)
320+
308321 ctx, svc, cleanup, err := sessionSetup(cmd)
309322 if err != nil {
310323 return err
@@ -338,6 +351,9 @@ func runSessionRename(cmd *cobra.Command, args []string) error {
338351}
339352
340353func runSessionLast(cmd *cobra.Command, _ []string) error {
354+ event.SetNonInteractive(true)
355+ event.SessionLastShown(sessionLastJSON)
356+
341357 ctx, svc, cleanup, err := sessionSetup(cmd)
342358 if err != nil {
343359 return err
internal/config/config.go+6
@@ -592,6 +592,12 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
592592 return nil
593593 }
594594 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")
595601 }
596602
597603 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) {
6363 if q.getHourDayHeatmapStmt, err = db.PrepareContext(ctx, getHourDayHeatmap); err != nil {
6464 return nil, fmt.Errorf("error preparing query GetHourDayHeatmap: %w", err)
6565 }
66+ if q.getLastSessionStmt, err = db.PrepareContext(ctx, getLastSession); err != nil {
67+ return nil, fmt.Errorf("error preparing query GetLastSession: %w", err)
68+ }
6669 if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
6770 return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
6871 }
@@ -202,6 +205,11 @@ func (q *Queries) Close() error {
202205 err = fmt.Errorf("error closing getHourDayHeatmapStmt: %w", cerr)
203206 }
204207 }
208+ if q.getLastSessionStmt != nil {
209+ if cerr := q.getLastSessionStmt.Close(); cerr != nil {
210+ err = fmt.Errorf("error closing getLastSessionStmt: %w", cerr)
211+ }
212+ }
205213 if q.getMessageStmt != nil {
206214 if cerr := q.getMessageStmt.Close(); cerr != nil {
207215 err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
@@ -369,6 +377,7 @@ type Queries struct {
369377 getFileByPathAndSessionStmt *sql.Stmt
370378 getFileReadStmt *sql.Stmt
371379 getHourDayHeatmapStmt *sql.Stmt
380+ getLastSessionStmt *sql.Stmt
372381 getMessageStmt *sql.Stmt
373382 getRecentActivityStmt *sql.Stmt
374383 getSessionByIDStmt *sql.Stmt
@@ -411,6 +420,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
411420 getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
412421 getFileReadStmt: q.getFileReadStmt,
413422 getHourDayHeatmapStmt: q.getHourDayHeatmapStmt,
423+ getLastSessionStmt: q.getLastSessionStmt,
414424 getMessageStmt: q.getMessageStmt,
415425 getRecentActivityStmt: q.getRecentActivityStmt,
416426 getSessionByIDStmt: q.getSessionByIDStmt,
internal/db/querier.go+1
@@ -22,6 +22,7 @@ type Querier interface {
2222 GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
2323 GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
2424 GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
25+ GetLastSession(ctx context.Context) (Session, error)
2526 GetMessage(ctx context.Context, id string) (Message, error)
2627 GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
2728 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 {
8383 return err
8484}
8585
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+
86112const getSessionByID = `-- name: GetSessionByID :one
87113SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
88114FROM sessions
internal/db/sql/sessions.sql+6
@@ -28,6 +28,12 @@ SELECT *
2828FROM sessions
2929WHERE id = ? LIMIT 1;
3030
31+-- name: GetLastSession :one
32+SELECT *
33+FROM sessions
34+ORDER BY updated_at DESC
35+LIMIT 1;
36+
3137-- name: ListSessions :many
3238SELECT *
3339FROM sessions
internal/event/all.go+20
@@ -61,3 +61,23 @@ func TokensUsed(props ...any) {
6161func StatsViewed() {
6262 send("stats viewed")
6363}
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 (
1717 endpoint = "https://data.charm.land"
1818 key = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
1919
20− nonInteractiveEventName = "NonInteractive"
20+ nonInteractiveAttrName = "NonInteractive"
21+ continueSessionByIDAttrName = "ContinueSessionByID"
22+ continueLastSessionAttrName = "ContinueLastSession"
2123)
2224
2325var (
@@ -30,11 +32,19 @@ var (
3032 Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
3133 Set("Version", version.Version).
3234 Set("GoVersion", runtime.Version()).
33− Set(nonInteractiveEventName, false)
35+ Set(nonInteractiveAttrName, false)
3436)
3537
3638func 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)
3848}
3949
4050func Init() {
internal/session/session.go+9
@@ -66,6 +66,7 @@ type Service interface {
6666 CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
6767 CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
6868 Get(ctx context.Context, id string) (Session, error)
69+ GetLast(ctx context.Context) (Session, error)
6970 List(ctx context.Context) ([]Session, error)
7071 Save(ctx context.Context, session Session) (Session, error)
7172 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) {
166167 return s.fromDBItem(dbSession), nil
167168}
168169
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+
169178func (s *service) Save(ctx context.Context, session Session) (Session, error) {
170179 todosJSON, err := marshalTodos(session.Todos)
171180 if err != nil {
internal/ui/chat/agent.go+2−2
@@ -101,7 +101,7 @@ type AgentToolRenderContext struct {
101101func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
102102 cappedWidth := cappedMessageWidth(width)
103103 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)
105105 }
106106
107107 var params agent.AgentParams
@@ -232,7 +232,7 @@ type agenticFetchParams struct {
232232func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
233233 cappedWidth := cappedMessageWidth(width)
234234 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)
236236 }
237237
238238 var params agenticFetchParams
internal/ui/chat/bash.go+3−3
@@ -41,7 +41,7 @@ type BashToolRenderContext struct{}
4141func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
4242 cappedWidth := cappedMessageWidth(width)
4343 if opts.IsPending() {
44− return pendingTool(sty, "Bash", opts.Anim)
44+ return pendingTool(sty, "Bash", opts.Anim, opts.Compact)
4545 }
4646
4747 var params tools.BashParams
@@ -123,7 +123,7 @@ type JobOutputToolRenderContext struct{}
123123func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
124124 cappedWidth := cappedMessageWidth(width)
125125 if opts.IsPending() {
126− return pendingTool(sty, "Job", opts.Anim)
126+ return pendingTool(sty, "Job", opts.Anim, opts.Compact)
127127 }
128128
129129 var params tools.JobOutputParams
@@ -174,7 +174,7 @@ type JobKillToolRenderContext struct{}
174174func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
175175 cappedWidth := cappedMessageWidth(width)
176176 if opts.IsPending() {
177− return pendingTool(sty, "Job", opts.Anim)
177+ return pendingTool(sty, "Job", opts.Anim, opts.Compact)
178178 }
179179
180180 var params tools.JobKillParams
internal/ui/chat/diagnostics.go+1−1
@@ -37,7 +37,7 @@ type DiagnosticsToolRenderContext struct{}
3737func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
3838 cappedWidth := cappedMessageWidth(width)
3939 if opts.IsPending() {
40− return pendingTool(sty, "Diagnostics", opts.Anim)
40+ return pendingTool(sty, "Diagnostics", opts.Anim, opts.Compact)
4141 }
4242
4343 var params tools.DiagnosticsParams
internal/ui/chat/fetch.go+3−3
@@ -36,7 +36,7 @@ type FetchToolRenderContext struct{}
3636func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
3737 cappedWidth := cappedMessageWidth(width)
3838 if opts.IsPending() {
39− return pendingTool(sty, "Fetch", opts.Anim)
39+ return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
4040 }
4141
4242 var params tools.FetchParams
@@ -111,7 +111,7 @@ type WebFetchToolRenderContext struct{}
111111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
112112 cappedWidth := cappedMessageWidth(width)
113113 if opts.IsPending() {
114− return pendingTool(sty, "Fetch", opts.Anim)
114+ return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
115115 }
116116
117117 var params tools.WebFetchParams
@@ -165,7 +165,7 @@ type WebSearchToolRenderContext struct{}
165165func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
166166 cappedWidth := cappedMessageWidth(width)
167167 if opts.IsPending() {
168− return pendingTool(sty, "Search", opts.Anim)
168+ return pendingTool(sty, "Search", opts.Anim, opts.Compact)
169169 }
170170
171171 var params tools.WebSearchParams
internal/ui/chat/file.go+5−5
@@ -39,7 +39,7 @@ type ViewToolRenderContext struct{}
3939func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
4040 cappedWidth := cappedMessageWidth(width)
4141 if opts.IsPending() {
42− return pendingTool(sty, "View", opts.Anim)
42+ return pendingTool(sty, "View", opts.Anim, opts.Compact)
4343 }
4444
4545 var params tools.ViewParams
@@ -125,7 +125,7 @@ type WriteToolRenderContext struct{}
125125func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
126126 cappedWidth := cappedMessageWidth(width)
127127 if opts.IsPending() {
128− return pendingTool(sty, "Write", opts.Anim)
128+ return pendingTool(sty, "Write", opts.Anim, opts.Compact)
129129 }
130130
131131 var params tools.WriteParams
@@ -180,7 +180,7 @@ type EditToolRenderContext struct{}
180180func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
181181 // Edit tool uses full width for diffs.
182182 if opts.IsPending() {
183− return pendingTool(sty, "Edit", opts.Anim)
183+ return pendingTool(sty, "Edit", opts.Anim, opts.Compact)
184184 }
185185
186186 var params tools.EditParams
@@ -243,7 +243,7 @@ type MultiEditToolRenderContext struct{}
243243func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
244244 // MultiEdit tool uses full width for diffs.
245245 if opts.IsPending() {
246− return pendingTool(sty, "Multi-Edit", opts.Anim)
246+ return pendingTool(sty, "Multi-Edit", opts.Anim, opts.Compact)
247247 }
248248
249249 var params tools.MultiEditParams
@@ -311,7 +311,7 @@ type DownloadToolRenderContext struct{}
311311func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
312312 cappedWidth := cappedMessageWidth(width)
313313 if opts.IsPending() {
314− return pendingTool(sty, "Download", opts.Anim)
314+ return pendingTool(sty, "Download", opts.Anim, opts.Compact)
315315 }
316316
317317 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
3535 name := genericPrettyName(opts.ToolCall.Name)
3636
3737 if opts.IsPending() {
38− return pendingTool(sty, name, opts.Anim)
38+ return pendingTool(sty, name, opts.Anim, opts.Compact)
3939 }
4040
4141 var params map[string]any
internal/ui/chat/lsp_restart.go+1−1
@@ -32,7 +32,7 @@ type LSPRestartToolRenderContext struct{}
3232func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
3333 cappedWidth := cappedMessageWidth(width)
3434 if opts.IsPending() {
35− return pendingTool(sty, "Restart LSP", opts.Anim)
35+ return pendingTool(sty, "Restart LSP", opts.Anim, opts.Compact)
3636 }
3737
3838 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
4646 name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
4747
4848 if opts.IsPending() {
49− return pendingTool(sty, name, opts.Anim)
49+ return pendingTool(sty, name, opts.Anim, opts.Compact)
5050 }
5151
5252 var params map[string]any
internal/ui/chat/references.go+1−1
@@ -33,7 +33,7 @@ type ReferencesToolRenderContext struct{}
3333func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
3434 cappedWidth := cappedMessageWidth(width)
3535 if opts.IsPending() {
36− return pendingTool(sty, "Find References", opts.Anim)
36+ return pendingTool(sty, "Find References", opts.Anim, opts.Compact)
3737 }
3838
3939 var params tools.ReferencesParams
internal/ui/chat/search.go+4−4
@@ -37,7 +37,7 @@ type GlobToolRenderContext struct{}
3737func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
3838 cappedWidth := cappedMessageWidth(width)
3939 if opts.IsPending() {
40− return pendingTool(sty, "Glob", opts.Anim)
40+ return pendingTool(sty, "Glob", opts.Anim, opts.Compact)
4141 }
4242
4343 var params tools.GlobParams
@@ -96,7 +96,7 @@ type GrepToolRenderContext struct{}
9696func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
9797 cappedWidth := cappedMessageWidth(width)
9898 if opts.IsPending() {
99− return pendingTool(sty, "Grep", opts.Anim)
99+ return pendingTool(sty, "Grep", opts.Anim, opts.Compact)
100100 }
101101
102102 var params tools.GrepParams
@@ -161,7 +161,7 @@ type LSToolRenderContext struct{}
161161func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
162162 cappedWidth := cappedMessageWidth(width)
163163 if opts.IsPending() {
164− return pendingTool(sty, "List", opts.Anim)
164+ return pendingTool(sty, "List", opts.Anim, opts.Compact)
165165 }
166166
167167 var params tools.LSParams
@@ -221,7 +221,7 @@ type SourcegraphToolRenderContext struct{}
221221func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
222222 cappedWidth := cappedMessageWidth(width)
223223 if opts.IsPending() {
224− return pendingTool(sty, "Sourcegraph", opts.Anim)
224+ return pendingTool(sty, "Sourcegraph", opts.Anim, opts.Compact)
225225 }
226226
227227 var params tools.SourcegraphParams
internal/ui/chat/todos.go+1−1
@@ -41,7 +41,7 @@ type TodosToolRenderContext struct{}
4141func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
4242 cappedWidth := cappedMessageWidth(width)
4343 if opts.IsPending() {
44− return pendingTool(sty, "To-Do", opts.Anim)
44+ return pendingTool(sty, "To-Do", opts.Anim, opts.Compact)
4545 }
4646
4747 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) {
424424}
425425
426426// 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 {
428428 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)
430434
431435 var animView string
432436 if anim != nil {
internal/ui/dialog/models_list.go+3−1
@@ -216,8 +216,10 @@ func (f *ModelsList) VisibleItems() []list.Item {
216216 }
217217
218218 matches := fuzzy.Find(query, names)
219+
220+ // Sort by original index to preserve order within the group
219221 sort.SliceStable(matches, func(i, j int) bool {
220− return matches[i].Score > matches[j].Score
222+ return matches[i].Index < matches[j].Index
221223 })
222224
223225 for _, match := range matches {
internal/ui/styles/styles.go+77−81
@@ -254,18 +254,18 @@ type Styles struct {
254254 // Tool - styles for tool call rendering
255255 Tool struct {
256256 // Icon styles with tool status
257− IconPending lipgloss.Style // Pending operation icon
258− IconSuccess lipgloss.Style // Successful operation icon
259− IconError lipgloss.Style // Error operation icon
260− IconCancelled lipgloss.Style // Cancelled operation icon
257+ IconPending lipgloss.Style
258+ IconSuccess lipgloss.Style
259+ IconError lipgloss.Style
260+ IconCancelled lipgloss.Style
261261
262262 // Tool name styles
263− NameNormal lipgloss.Style // Normal tool name
264− NameNested lipgloss.Style // Nested tool name
263+ NameNormal lipgloss.Style // Top-level tool name
264+ NameNested lipgloss.Style // Nested child tool name (inside Agent/Agentic Fetch)
265265
266266 // Parameter list styles
267− ParamMain lipgloss.Style // Main parameter
268− ParamKey lipgloss.Style // Parameter keys
267+ ParamMain lipgloss.Style
268+ ParamKey lipgloss.Style
269269
270270 // Content rendering styles
271271 ContentLine lipgloss.Style // Individual content line with background and width
@@ -632,14 +632,14 @@ func DefaultStyles() Styles {
632632 StylePrimitive: ansi.StylePrimitive{
633633 // BlockPrefix: "\n",
634634 // BlockSuffix: "\n",
635− Color: stringPtr(charmtone.Smoke.Hex()),
635+ Color: new(charmtone.Smoke.Hex()),
636636 },
637− // Margin: uintPtr(defaultMargin),
637+ // Margin: new(uint(defaultMargin)),
638638 },
639639 BlockQuote: ansi.StyleBlock{
640640 StylePrimitive: ansi.StylePrimitive{},
641− Indent: uintPtr(1),
642− IndentToken: stringPtr("│ "),
641+ Indent: new(uint(1)),
642+ IndentToken: new("│ "),
643643 },
644644 List: ansi.StyleList{
645645 LevelIndent: defaultListIndent,
@@ -647,17 +647,17 @@ func DefaultStyles() Styles {
647647 Heading: ansi.StyleBlock{
648648 StylePrimitive: ansi.StylePrimitive{
649649 BlockSuffix: "\n",
650− Color: stringPtr(charmtone.Malibu.Hex()),
651− Bold: boolPtr(true),
650+ Color: new(charmtone.Malibu.Hex()),
651+ Bold: new(true),
652652 },
653653 },
654654 H1: ansi.StyleBlock{
655655 StylePrimitive: ansi.StylePrimitive{
656656 Prefix: " ",
657657 Suffix: " ",
658− Color: stringPtr(charmtone.Zest.Hex()),
659− BackgroundColor: stringPtr(charmtone.Charple.Hex()),
660− Bold: boolPtr(true),
658+ Color: new(charmtone.Zest.Hex()),
659+ BackgroundColor: new(charmtone.Charple.Hex()),
660+ Bold: new(true),
661661 },
662662 },
663663 H2: ansi.StyleBlock{
@@ -683,21 +683,21 @@ func DefaultStyles() Styles {
683683 H6: ansi.StyleBlock{
684684 StylePrimitive: ansi.StylePrimitive{
685685 Prefix: "###### ",
686− Color: stringPtr(charmtone.Guac.Hex()),
687− Bold: boolPtr(false),
686+ Color: new(charmtone.Guac.Hex()),
687+ Bold: new(false),
688688 },
689689 },
690690 Strikethrough: ansi.StylePrimitive{
691− CrossedOut: boolPtr(true),
691+ CrossedOut: new(true),
692692 },
693693 Emph: ansi.StylePrimitive{
694− Italic: boolPtr(true),
694+ Italic: new(true),
695695 },
696696 Strong: ansi.StylePrimitive{
697− Bold: boolPtr(true),
697+ Bold: new(true),
698698 },
699699 HorizontalRule: ansi.StylePrimitive{
700− Color: stringPtr(charmtone.Charcoal.Hex()),
700+ Color: new(charmtone.Charcoal.Hex()),
701701 Format: "\n--------\n",
702702 },
703703 Item: ansi.StylePrimitive{
@@ -712,117 +712,117 @@ func DefaultStyles() Styles {
712712 Unticked: "[ ] ",
713713 },
714714 Link: ansi.StylePrimitive{
715− Color: stringPtr(charmtone.Zinc.Hex()),
716− Underline: boolPtr(true),
715+ Color: new(charmtone.Zinc.Hex()),
716+ Underline: new(true),
717717 },
718718 LinkText: ansi.StylePrimitive{
719− Color: stringPtr(charmtone.Guac.Hex()),
720− Bold: boolPtr(true),
719+ Color: new(charmtone.Guac.Hex()),
720+ Bold: new(true),
721721 },
722722 Image: ansi.StylePrimitive{
723− Color: stringPtr(charmtone.Cheeky.Hex()),
724− Underline: boolPtr(true),
723+ Color: new(charmtone.Cheeky.Hex()),
724+ Underline: new(true),
725725 },
726726 ImageText: ansi.StylePrimitive{
727− Color: stringPtr(charmtone.Squid.Hex()),
727+ Color: new(charmtone.Squid.Hex()),
728728 Format: "Image: {{.text}} →",
729729 },
730730 Code: ansi.StyleBlock{
731731 StylePrimitive: ansi.StylePrimitive{
732732 Prefix: " ",
733733 Suffix: " ",
734− Color: stringPtr(charmtone.Coral.Hex()),
735− BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
734+ Color: new(charmtone.Coral.Hex()),
735+ BackgroundColor: new(charmtone.Charcoal.Hex()),
736736 },
737737 },
738738 CodeBlock: ansi.StyleCodeBlock{
739739 StyleBlock: ansi.StyleBlock{
740740 StylePrimitive: ansi.StylePrimitive{
741− Color: stringPtr(charmtone.Charcoal.Hex()),
741+ Color: new(charmtone.Charcoal.Hex()),
742742 },
743− Margin: uintPtr(defaultMargin),
743+ Margin: new(uint(defaultMargin)),
744744 },
745745 Chroma: &ansi.Chroma{
746746 Text: ansi.StylePrimitive{
747− Color: stringPtr(charmtone.Smoke.Hex()),
747+ Color: new(charmtone.Smoke.Hex()),
748748 },
749749 Error: ansi.StylePrimitive{
750− Color: stringPtr(charmtone.Butter.Hex()),
751− BackgroundColor: stringPtr(charmtone.Sriracha.Hex()),
750+ Color: new(charmtone.Butter.Hex()),
751+ BackgroundColor: new(charmtone.Sriracha.Hex()),
752752 },
753753 Comment: ansi.StylePrimitive{
754− Color: stringPtr(charmtone.Oyster.Hex()),
754+ Color: new(charmtone.Oyster.Hex()),
755755 },
756756 CommentPreproc: ansi.StylePrimitive{
757− Color: stringPtr(charmtone.Bengal.Hex()),
757+ Color: new(charmtone.Bengal.Hex()),
758758 },
759759 Keyword: ansi.StylePrimitive{
760− Color: stringPtr(charmtone.Malibu.Hex()),
760+ Color: new(charmtone.Malibu.Hex()),
761761 },
762762 KeywordReserved: ansi.StylePrimitive{
763− Color: stringPtr(charmtone.Pony.Hex()),
763+ Color: new(charmtone.Pony.Hex()),
764764 },
765765 KeywordNamespace: ansi.StylePrimitive{
766− Color: stringPtr(charmtone.Pony.Hex()),
766+ Color: new(charmtone.Pony.Hex()),
767767 },
768768 KeywordType: ansi.StylePrimitive{
769− Color: stringPtr(charmtone.Guppy.Hex()),
769+ Color: new(charmtone.Guppy.Hex()),
770770 },
771771 Operator: ansi.StylePrimitive{
772− Color: stringPtr(charmtone.Salmon.Hex()),
772+ Color: new(charmtone.Salmon.Hex()),
773773 },
774774 Punctuation: ansi.StylePrimitive{
775− Color: stringPtr(charmtone.Zest.Hex()),
775+ Color: new(charmtone.Zest.Hex()),
776776 },
777777 Name: ansi.StylePrimitive{
778− Color: stringPtr(charmtone.Smoke.Hex()),
778+ Color: new(charmtone.Smoke.Hex()),
779779 },
780780 NameBuiltin: ansi.StylePrimitive{
781− Color: stringPtr(charmtone.Cheeky.Hex()),
781+ Color: new(charmtone.Cheeky.Hex()),
782782 },
783783 NameTag: ansi.StylePrimitive{
784− Color: stringPtr(charmtone.Mauve.Hex()),
784+ Color: new(charmtone.Mauve.Hex()),
785785 },
786786 NameAttribute: ansi.StylePrimitive{
787− Color: stringPtr(charmtone.Hazy.Hex()),
787+ Color: new(charmtone.Hazy.Hex()),
788788 },
789789 NameClass: ansi.StylePrimitive{
790− Color: stringPtr(charmtone.Salt.Hex()),
791− Underline: boolPtr(true),
792− Bold: boolPtr(true),
790+ Color: new(charmtone.Salt.Hex()),
791+ Underline: new(true),
792+ Bold: new(true),
793793 },
794794 NameDecorator: ansi.StylePrimitive{
795− Color: stringPtr(charmtone.Citron.Hex()),
795+ Color: new(charmtone.Citron.Hex()),
796796 },
797797 NameFunction: ansi.StylePrimitive{
798− Color: stringPtr(charmtone.Guac.Hex()),
798+ Color: new(charmtone.Guac.Hex()),
799799 },
800800 LiteralNumber: ansi.StylePrimitive{
801− Color: stringPtr(charmtone.Julep.Hex()),
801+ Color: new(charmtone.Julep.Hex()),
802802 },
803803 LiteralString: ansi.StylePrimitive{
804− Color: stringPtr(charmtone.Cumin.Hex()),
804+ Color: new(charmtone.Cumin.Hex()),
805805 },
806806 LiteralStringEscape: ansi.StylePrimitive{
807− Color: stringPtr(charmtone.Bok.Hex()),
807+ Color: new(charmtone.Bok.Hex()),
808808 },
809809 GenericDeleted: ansi.StylePrimitive{
810− Color: stringPtr(charmtone.Coral.Hex()),
810+ Color: new(charmtone.Coral.Hex()),
811811 },
812812 GenericEmph: ansi.StylePrimitive{
813− Italic: boolPtr(true),
813+ Italic: new(true),
814814 },
815815 GenericInserted: ansi.StylePrimitive{
816− Color: stringPtr(charmtone.Guac.Hex()),
816+ Color: new(charmtone.Guac.Hex()),
817817 },
818818 GenericStrong: ansi.StylePrimitive{
819− Bold: boolPtr(true),
819+ Bold: new(true),
820820 },
821821 GenericSubheading: ansi.StylePrimitive{
822− Color: stringPtr(charmtone.Squid.Hex()),
822+ Color: new(charmtone.Squid.Hex()),
823823 },
824824 Background: ansi.StylePrimitive{
825− BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
825+ BackgroundColor: new(charmtone.Charcoal.Hex()),
826826 },
827827 },
828828 },
@@ -837,8 +837,8 @@ func DefaultStyles() Styles {
837837 }
838838
839839 // PlainMarkdown style - muted colors on subtle background for thinking content.
840− plainBg := stringPtr(bgBaseLighter.Hex())
841− plainFg := stringPtr(fgMuted.Hex())
840+ plainBg := new(bgBaseLighter.Hex())
841+ plainFg := new(fgMuted.Hex())
842842 s.PlainMarkdown = ansi.StyleConfig{
843843 Document: ansi.StyleBlock{
844844 StylePrimitive: ansi.StylePrimitive{
@@ -851,8 +851,8 @@ func DefaultStyles() Styles {
851851 Color: plainFg,
852852 BackgroundColor: plainBg,
853853 },
854− Indent: uintPtr(1),
855− IndentToken: stringPtr("│ "),
854+ Indent: new(uint(1)),
855+ IndentToken: new("│ "),
856856 },
857857 List: ansi.StyleList{
858858 LevelIndent: defaultListIndent,
@@ -860,7 +860,7 @@ func DefaultStyles() Styles {
860860 Heading: ansi.StyleBlock{
861861 StylePrimitive: ansi.StylePrimitive{
862862 BlockSuffix: "\n",
863− Bold: boolPtr(true),
863+ Bold: new(true),
864864 Color: plainFg,
865865 BackgroundColor: plainBg,
866866 },
@@ -869,7 +869,7 @@ func DefaultStyles() Styles {
869869 StylePrimitive: ansi.StylePrimitive{
870870 Prefix: " ",
871871 Suffix: " ",
872− Bold: boolPtr(true),
872+ Bold: new(true),
873873 Color: plainFg,
874874 BackgroundColor: plainBg,
875875 },
@@ -910,17 +910,17 @@ func DefaultStyles() Styles {
910910 },
911911 },
912912 Strikethrough: ansi.StylePrimitive{
913− CrossedOut: boolPtr(true),
913+ CrossedOut: new(true),
914914 Color: plainFg,
915915 BackgroundColor: plainBg,
916916 },
917917 Emph: ansi.StylePrimitive{
918− Italic: boolPtr(true),
918+ Italic: new(true),
919919 Color: plainFg,
920920 BackgroundColor: plainBg,
921921 },
922922 Strong: ansi.StylePrimitive{
923− Bold: boolPtr(true),
923+ Bold: new(true),
924924 Color: plainFg,
925925 BackgroundColor: plainBg,
926926 },
@@ -948,17 +948,17 @@ func DefaultStyles() Styles {
948948 Unticked: "[ ] ",
949949 },
950950 Link: ansi.StylePrimitive{
951− Underline: boolPtr(true),
951+ Underline: new(true),
952952 Color: plainFg,
953953 BackgroundColor: plainBg,
954954 },
955955 LinkText: ansi.StylePrimitive{
956− Bold: boolPtr(true),
956+ Bold: new(true),
957957 Color: plainFg,
958958 BackgroundColor: plainBg,
959959 },
960960 Image: ansi.StylePrimitive{
961− Underline: boolPtr(true),
961+ Underline: new(true),
962962 Color: plainFg,
963963 BackgroundColor: plainBg,
964964 },
@@ -981,7 +981,7 @@ func DefaultStyles() Styles {
981981 Color: plainFg,
982982 BackgroundColor: plainBg,
983983 },
984− Margin: uintPtr(defaultMargin),
984+ Margin: new(uint(defaultMargin)),
985985 },
986986 },
987987 Table: ansi.StyleTable{
@@ -1119,7 +1119,7 @@ func DefaultStyles() Styles {
11191119 s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
11201120
11211121 s.Tool.NameNormal = base.Foreground(blue)
1122− s.Tool.NameNested = base.Foreground(fgHalfMuted)
1122+ s.Tool.NameNested = base.Foreground(blue)
11231123
11241124 s.Tool.ParamMain = s.Subtle
11251125 s.Tool.ParamKey = s.Subtle
@@ -1368,10 +1368,6 @@ func DefaultStyles() Styles {
13681368 return s
13691369}
13701370
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 }
13751371func chromaStyle(style ansi.StylePrimitive) string {
13761372 var s strings.Builder
13771373