Merge remote-tracking branch 'origin/main' into server-client-2

Ayman Bagabas created

Change summary

.github/workflows/security.yml       |  12 +-
Taskfile.yaml                        |   5 
go.mod                               |   2 
go.sum                               |   4 
internal/app/app.go                  |  42 ++++++
internal/app/resolve_session_test.go | 174 ++++++++++++++++++++++++++++++
internal/cmd/run.go                  | 116 ++++++++++++++++++-
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              |  16 ++
internal/session/session.go          |   9 +
internal/ui/chat/agent.go            |   4 
internal/ui/chat/bash.go             |   6 
internal/ui/chat/diagnostics.go      |   2 
internal/ui/chat/fetch.go            |   6 
internal/ui/chat/file.go             |  10 
internal/ui/chat/generic.go          |   2 
internal/ui/chat/lsp_restart.go      |   2 
internal/ui/chat/mcp.go              |   2 
internal/ui/chat/references.go       |   2 
internal/ui/chat/search.go           |   8 
internal/ui/chat/todos.go            |   2 
internal/ui/chat/tools.go            |   8 +
internal/ui/dialog/models_list.go    |   4 
internal/ui/styles/styles.go         | 158 +++++++++++++-------------
30 files changed, 551 insertions(+), 130 deletions(-)

Detailed changes

.github/workflows/security.yml 🔗

@@ -30,11 +30,11 @@ jobs:
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           persist-credentials: false
-      - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+      - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
         with:
           languages: ${{ matrix.language }}
-      - uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
-      - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+      - uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
+      - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
 
   grype:
     runs-on: ubuntu-latest
@@ -52,7 +52,7 @@ jobs:
           path: "."
           fail-build: true
           severity-cutoff: critical
-      - uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+      - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
         with:
           sarif_file: ${{ steps.scan.outputs.sarif }}
 
@@ -73,7 +73,7 @@ jobs:
       - name: Run govulncheck
         run: |
           govulncheck -C . -format sarif ./... > results.sarif
-      - uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+      - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
         with:
           sarif_file: results.sarif
 
@@ -86,7 +86,7 @@ jobs:
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           persist-credentials: false
-      - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
+      - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
         with:
           fail-on-severity: critical
           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 🔗

@@ -184,3 +184,8 @@ tasks:
       - go get charm.land/fantasy
       - go get charm.land/catwalk
       - go mod tidy
+
+  sqlc:
+    desc: Generate code using SQLC
+    cmds:
+      - sqlc generate

go.mod 🔗

@@ -5,7 +5,7 @@ go 1.26.1
 require (
 	charm.land/bubbles/v2 v2.0.0
 	charm.land/bubbletea/v2 v2.0.2
-	charm.land/catwalk v0.30.0
+	charm.land/catwalk v0.30.3
 	charm.land/fang/v2 v2.0.1
 	charm.land/fantasy v0.12.3
 	charm.land/glamour/v2 v2.0.0

go.sum 🔗

@@ -2,8 +2,8 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
 charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
 charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
 charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
-charm.land/catwalk v0.30.0 h1:CgWN3hM0HYYXlxMFsd1mfpBliqvx9gH3LolVi1VfMSU=
-charm.land/catwalk v0.30.0/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
+charm.land/catwalk v0.30.3 h1:eCRwVoi1znrNGYiPZoBIbWt8+Q4kDhT3zziqnPO3s2Y=
+charm.land/catwalk v0.30.3/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg=
 charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
 charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
 charm.land/fantasy v0.12.3 h1:gvqRWD7vWmpNN0VcQ+rSku5QdTlLegqrlJDVCDdAh58=

internal/app/app.go 🔗

@@ -171,9 +171,40 @@ func (app *App) AgentNotifications() *pubsub.Broker[notify.Notification] {
 	return app.agentNotifications
 }
 
+// resolveSession resolves which session to use for a non-interactive run
+// If continueSessionID is set, it looks up that session by ID
+// If useLast is set, it returns the most recently updated top-level session
+// Otherwise, it creates a new session
+func (app *App) resolveSession(ctx context.Context, continueSessionID string, useLast bool) (session.Session, error) {
+	switch {
+	case continueSessionID != "":
+		if app.Sessions.IsAgentToolSession(continueSessionID) {
+			return session.Session{}, fmt.Errorf("cannot continue an agent tool session: %s", continueSessionID)
+		}
+		sess, err := app.Sessions.Get(ctx, continueSessionID)
+		if err != nil {
+			return session.Session{}, fmt.Errorf("session not found: %s", continueSessionID)
+		}
+		if sess.ParentSessionID != "" {
+			return session.Session{}, fmt.Errorf("cannot continue a child session: %s", continueSessionID)
+		}
+		return sess, nil
+
+	case useLast:
+		sess, err := app.Sessions.GetLast(ctx)
+		if err != nil {
+			return session.Session{}, fmt.Errorf("no sessions found to continue")
+		}
+		return sess, nil
+
+	default:
+		return app.Sessions.Create(ctx, agent.DefaultSessionName)
+	}
+}
+
 // RunNonInteractive runs the application in non-interactive mode with the
 // given prompt, printing to stdout.
-func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
+func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool, continueSessionID string, useLast bool) error {
 	slog.Info("Running in non-interactive mode")
 
 	ctx, cancel := context.WithCancel(ctx)
@@ -241,11 +272,16 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
 
 	defer stopSpinner()
 
-	sess, err := app.Sessions.Create(ctx, agent.DefaultSessionName)
+	sess, err := app.resolveSession(ctx, continueSessionID, useLast)
 	if err != nil {
 		return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
 	}
-	slog.Info("Created session for non-interactive run", "session_id", sess.ID)
+
+	if continueSessionID != "" || useLast {
+		slog.Info("Continuing session for non-interactive run", "session_id", sess.ID)
+	} else {
+		slog.Info("Created session for non-interactive run", "session_id", sess.ID)
+	}
 
 	// Automatically approve all permission requests for this non-interactive
 	// session.

internal/app/resolve_session_test.go 🔗

@@ -0,0 +1,174 @@
+package app
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/charmbracelet/crush/internal/pubsub"
+	"github.com/charmbracelet/crush/internal/session"
+	"github.com/stretchr/testify/require"
+)
+
+// mockSessionService is a minimal mock of session.Service for testing resolveSession.
+type mockSessionService struct {
+	sessions []session.Session
+	created  []session.Session
+}
+
+func (m *mockSessionService) Subscribe(context.Context) <-chan pubsub.Event[session.Session] {
+	return make(chan pubsub.Event[session.Session])
+}
+
+func (m *mockSessionService) Create(_ context.Context, title string) (session.Session, error) {
+	s := session.Session{ID: "new-session-id", Title: title}
+	m.created = append(m.created, s)
+	return s, nil
+}
+
+func (m *mockSessionService) CreateTitleSession(context.Context, string) (session.Session, error) {
+	return session.Session{}, nil
+}
+
+func (m *mockSessionService) CreateTaskSession(context.Context, string, string, string) (session.Session, error) {
+	return session.Session{}, nil
+}
+
+func (m *mockSessionService) Get(_ context.Context, id string) (session.Session, error) {
+	for _, s := range m.sessions {
+		if s.ID == id {
+			return s, nil
+		}
+	}
+	return session.Session{}, sql.ErrNoRows
+}
+
+func (m *mockSessionService) GetLast(_ context.Context) (session.Session, error) {
+	if len(m.sessions) > 0 {
+		return m.sessions[0], nil
+	}
+	return session.Session{}, sql.ErrNoRows
+}
+
+func (m *mockSessionService) List(context.Context) ([]session.Session, error) {
+	return m.sessions, nil
+}
+
+func (m *mockSessionService) Save(_ context.Context, s session.Session) (session.Session, error) {
+	return s, nil
+}
+
+func (m *mockSessionService) UpdateTitleAndUsage(context.Context, string, string, int64, int64, float64) error {
+	return nil
+}
+
+func (m *mockSessionService) Rename(context.Context, string, string) error {
+	return nil
+}
+
+func (m *mockSessionService) Delete(context.Context, string) error {
+	return nil
+}
+
+func (m *mockSessionService) CreateAgentToolSessionID(messageID, toolCallID string) string {
+	return fmt.Sprintf("%s$$%s", messageID, toolCallID)
+}
+
+func (m *mockSessionService) ParseAgentToolSessionID(sessionID string) (string, string, bool) {
+	parts := strings.Split(sessionID, "$$")
+	if len(parts) != 2 {
+		return "", "", false
+	}
+	return parts[0], parts[1], true
+}
+
+func (m *mockSessionService) IsAgentToolSession(sessionID string) bool {
+	_, _, ok := m.ParseAgentToolSessionID(sessionID)
+	return ok
+}
+
+func newTestApp(sessions session.Service) *App {
+	return &App{Sessions: sessions}
+}
+
+func TestResolveSession_NewSession(t *testing.T) {
+	mock := &mockSessionService{}
+	app := newTestApp(mock)
+
+	sess, err := app.resolveSession(t.Context(), "", false)
+	require.NoError(t, err)
+	require.Equal(t, "new-session-id", sess.ID)
+	require.Len(t, mock.created, 1)
+}
+
+func TestResolveSession_ContinueByID(t *testing.T) {
+	mock := &mockSessionService{
+		sessions: []session.Session{
+			{ID: "existing-id", Title: "Old session"},
+		},
+	}
+	app := newTestApp(mock)
+
+	sess, err := app.resolveSession(t.Context(), "existing-id", false)
+	require.NoError(t, err)
+	require.Equal(t, "existing-id", sess.ID)
+	require.Equal(t, "Old session", sess.Title)
+	require.Empty(t, mock.created)
+}
+
+func TestResolveSession_ContinueByID_NotFound(t *testing.T) {
+	mock := &mockSessionService{}
+	app := newTestApp(mock)
+
+	_, err := app.resolveSession(t.Context(), "nonexistent", false)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "session not found")
+}
+
+func TestResolveSession_ContinueByID_ChildSession(t *testing.T) {
+	mock := &mockSessionService{
+		sessions: []session.Session{
+			{ID: "child-id", ParentSessionID: "parent-id", Title: "Child session"},
+		},
+	}
+	app := newTestApp(mock)
+
+	_, err := app.resolveSession(t.Context(), "child-id", false)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "cannot continue a child session")
+}
+
+func TestResolveSession_ContinueByID_AgentToolSession(t *testing.T) {
+	mock := &mockSessionService{}
+	app := newTestApp(mock)
+
+	_, err := app.resolveSession(t.Context(), "msg123$$tool456", false)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "cannot continue an agent tool session")
+}
+
+func TestResolveSession_Last(t *testing.T) {
+	mock := &mockSessionService{
+		sessions: []session.Session{
+			{ID: "most-recent", Title: "Latest session"},
+			{ID: "older", Title: "Older session"},
+		},
+	}
+	app := newTestApp(mock)
+
+	sess, err := app.resolveSession(t.Context(), "", true)
+	require.NoError(t, err)
+	require.Equal(t, "most-recent", sess.ID)
+	require.Empty(t, mock.created)
+}
+
+func TestResolveSession_Last_NoSessions(t *testing.T) {
+	mock := &mockSessionService{}
+	app := newTestApp(mock)
+
+	_, err := app.resolveSession(t.Context(), "", true)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "no sessions found")
+}

internal/cmd/run.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/charmbracelet/crush/internal/format"
 	"github.com/charmbracelet/crush/internal/proto"
 	"github.com/charmbracelet/crush/internal/pubsub"
+	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/ui/anim"
 	"github.com/charmbracelet/crush/internal/ui/styles"
 	"github.com/charmbracelet/x/ansi"
@@ -48,12 +49,23 @@ crush run --quiet "Generate a README for this project"
 
 # Run in verbose mode (show logs)
 crush run --verbose "Generate a README for this project"
+
+# Continue a previous session
+crush run --session {session-id} "Follow up on your last response"
+
+# Continue the most recent session
+crush run --continue "Follow up on your last response"
+
   `,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		quiet, _ := cmd.Flags().GetBool("quiet")
-		verbose, _ := cmd.Flags().GetBool("verbose")
-		largeModel, _ := cmd.Flags().GetString("model")
-		smallModel, _ := cmd.Flags().GetString("small-model")
+		var (
+			quiet, _      = cmd.Flags().GetBool("quiet")
+			verbose, _    = cmd.Flags().GetBool("verbose")
+			largeModel, _ = cmd.Flags().GetString("model")
+			smallModel, _ = cmd.Flags().GetString("small-model")
+			sessionID, _  = cmd.Flags().GetString("session")
+			useLast, _    = cmd.Flags().GetBool("continue")
+		)
 
 		// Cancel on SIGINT or SIGTERM.
 		ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
@@ -65,6 +77,14 @@ crush run --verbose "Generate a README for this project"
 		}
 		defer cleanup()
 
+		if sessionID != "" {
+			sess, err := resolveSessionByID(ctx, c, ws.ID, sessionID)
+			if err != nil {
+				return err
+			}
+			sessionID = sess.ID
+		}
+
 		if !ws.Config.IsConfigured() {
 			return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
 		}
@@ -88,7 +108,14 @@ crush run --verbose "Generate a README for this project"
 		event.SetNonInteractive(true)
 		event.AppInitialized()
 
-		return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose)
+		switch {
+		case sessionID != "":
+			event.SetContinueBySessionID(true)
+		case useLast:
+			event.SetContinueLastSession(true)
+		}
+
+		return runNonInteractive(ctx, c, ws, prompt, largeModel, smallModel, quiet || verbose, sessionID, useLast)
 	},
 }
 
@@ -97,6 +124,9 @@ func init() {
 	runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
 	runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
 	runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
+	runCmd.Flags().StringP("session", "s", "", "Continue a previous session by ID")
+	runCmd.Flags().BoolP("continue", "C", false, "Continue the most recent session")
+	runCmd.MarkFlagsMutuallyExclusive("session", "continue")
 }
 
 // runNonInteractive executes the agent via the server and streams output
@@ -107,6 +137,8 @@ func runNonInteractive(
 	ws *proto.Workspace,
 	prompt, largeModel, smallModel string,
 	hideSpinner bool,
+	continueSessionID string,
+	useLast bool,
 ) error {
 	slog.Info("Running in non-interactive mode")
 
@@ -172,11 +204,15 @@ func runNonInteractive(
 
 	defer stopSpinner()
 
-	sess, err := c.CreateSession(ctx, ws.ID, "non-interactive")
+	sess, err := resolveSession(ctx, c, ws.ID, continueSessionID, useLast)
 	if err != nil {
-		return fmt.Errorf("failed to create session: %w", err)
+		return fmt.Errorf("failed to resolve session: %w", err)
+	}
+	if continueSessionID != "" || useLast {
+		slog.Info("Continuing session for non-interactive run", "session_id", sess.ID)
+	} else {
+		slog.Info("Created session for non-interactive run", "session_id", sess.ID)
 	}
-	slog.Info("Created session for non-interactive run", "session_id", sess.ID)
 
 	events, err := c.SubscribeEvents(ctx, ws.ID)
 	if err != nil {
@@ -407,3 +443,67 @@ func validateModelMatches(matches []modelMatch, modelID, label string) (modelMat
 	}
 	return matches[0], nil
 }
+
+// resolveSession returns the session to use for a non-interactive run.
+// If continueSessionID is set it fetches that session; if useLast is set it
+// returns the most recently updated top-level session; otherwise it creates a
+// new one.
+func resolveSession(ctx context.Context, c *client.Client, wsID, continueSessionID string, useLast bool) (*session.Session, error) {
+	switch {
+	case continueSessionID != "":
+		sess, err := c.GetSession(ctx, wsID, continueSessionID)
+		if err != nil {
+			return nil, fmt.Errorf("session not found: %s", continueSessionID)
+		}
+		if sess.ParentSessionID != "" {
+			return nil, fmt.Errorf("cannot continue a child session: %s", continueSessionID)
+		}
+		return sess, nil
+
+	case useLast:
+		sessions, err := c.ListSessions(ctx, wsID)
+		if err != nil || len(sessions) == 0 {
+			return nil, fmt.Errorf("no sessions found to continue")
+		}
+		last := sessions[0]
+		for _, s := range sessions[1:] {
+			if s.UpdatedAt > last.UpdatedAt && s.ParentSessionID == "" {
+				last = s
+			}
+		}
+		return &last, nil
+
+	default:
+		return c.CreateSession(ctx, wsID, "non-interactive")
+	}
+}
+
+// resolveSessionByID resolves a session ID that may be a full UUID or a hash
+// prefix returned by crush session list.
+func resolveSessionByID(ctx context.Context, c *client.Client, wsID, id string) (*session.Session, error) {
+	if sess, err := c.GetSession(ctx, wsID, id); err == nil {
+		return sess, nil
+	}
+
+	sessions, err := c.ListSessions(ctx, wsID)
+	if err != nil {
+		return nil, err
+	}
+
+	var matches []session.Session
+	for _, s := range sessions {
+		hash := session.HashID(s.ID)
+		if hash == id || strings.HasPrefix(hash, id) {
+			matches = append(matches, s)
+		}
+	}
+
+	switch len(matches) {
+	case 0:
+		return nil, fmt.Errorf("session %q not found", id)
+	case 1:
+		return &matches[0], nil
+	default:
+		return nil, fmt.Errorf("session ID %q is ambiguous (%d matches)", id, len(matches))
+	}
+}

internal/cmd/session.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/charmbracelet/colorprofile"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/db"
+	"github.com/charmbracelet/crush/internal/event"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/ui/chat"
@@ -126,6 +127,9 @@ func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func()
 }
 
 func runSessionList(cmd *cobra.Command, _ []string) error {
+	event.SetNonInteractive(true)
+	event.SessionListed(sessionListJSON)
+
 	ctx, svc, cleanup, err := sessionSetup(cmd)
 	if err != nil {
 		return err
@@ -249,6 +253,9 @@ func resolveSessionID(ctx context.Context, svc session.Service, id string) (sess
 }
 
 func runSessionShow(cmd *cobra.Command, args []string) error {
+	event.SetNonInteractive(true)
+	event.SessionShown(sessionShowJSON)
+
 	ctx, svc, cleanup, err := sessionSetup(cmd)
 	if err != nil {
 		return err
@@ -273,6 +280,9 @@ func runSessionShow(cmd *cobra.Command, args []string) error {
 }
 
 func runSessionDelete(cmd *cobra.Command, args []string) error {
+	event.SetNonInteractive(true)
+	event.SessionDeletedCommand(sessionDeleteJSON)
+
 	ctx, svc, cleanup, err := sessionSetup(cmd)
 	if err != nil {
 		return err
@@ -305,6 +315,9 @@ func runSessionDelete(cmd *cobra.Command, args []string) error {
 }
 
 func runSessionRename(cmd *cobra.Command, args []string) error {
+	event.SetNonInteractive(true)
+	event.SessionRenamed(sessionRenameJSON)
+
 	ctx, svc, cleanup, err := sessionSetup(cmd)
 	if err != nil {
 		return err
@@ -338,6 +351,9 @@ func runSessionRename(cmd *cobra.Command, args []string) error {
 }
 
 func runSessionLast(cmd *cobra.Command, _ []string) error {
+	event.SetNonInteractive(true)
+	event.SessionLastShown(sessionLastJSON)
+
 	ctx, svc, cleanup, err := sessionSetup(cmd)
 	if err != nil {
 		return err

internal/config/config.go 🔗

@@ -592,6 +592,12 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
 			return nil
 		}
 		return errors.New("not a valid bedrock api key")
+	case catwalk.TypeVercel:
+		// NOTE: Vercel does not validate API keys on the `/models` endpoint.
+		if strings.HasPrefix(apiKey, "vck_") { // Vercel API keys
+			return nil
+		}
+		return errors.New("not a valid vercel api key")
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

internal/db/db.go 🔗

@@ -63,6 +63,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
 	if q.getHourDayHeatmapStmt, err = db.PrepareContext(ctx, getHourDayHeatmap); err != nil {
 		return nil, fmt.Errorf("error preparing query GetHourDayHeatmap: %w", err)
 	}
+	if q.getLastSessionStmt, err = db.PrepareContext(ctx, getLastSession); err != nil {
+		return nil, fmt.Errorf("error preparing query GetLastSession: %w", err)
+	}
 	if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
 		return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
 	}
@@ -202,6 +205,11 @@ func (q *Queries) Close() error {
 			err = fmt.Errorf("error closing getHourDayHeatmapStmt: %w", cerr)
 		}
 	}
+	if q.getLastSessionStmt != nil {
+		if cerr := q.getLastSessionStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing getLastSessionStmt: %w", cerr)
+		}
+	}
 	if q.getMessageStmt != nil {
 		if cerr := q.getMessageStmt.Close(); cerr != nil {
 			err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
@@ -369,6 +377,7 @@ type Queries struct {
 	getFileByPathAndSessionStmt    *sql.Stmt
 	getFileReadStmt                *sql.Stmt
 	getHourDayHeatmapStmt          *sql.Stmt
+	getLastSessionStmt             *sql.Stmt
 	getMessageStmt                 *sql.Stmt
 	getRecentActivityStmt          *sql.Stmt
 	getSessionByIDStmt             *sql.Stmt
@@ -411,6 +420,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
 		getFileByPathAndSessionStmt:    q.getFileByPathAndSessionStmt,
 		getFileReadStmt:                q.getFileReadStmt,
 		getHourDayHeatmapStmt:          q.getHourDayHeatmapStmt,
+		getLastSessionStmt:             q.getLastSessionStmt,
 		getMessageStmt:                 q.getMessageStmt,
 		getRecentActivityStmt:          q.getRecentActivityStmt,
 		getSessionByIDStmt:             q.getSessionByIDStmt,

internal/db/querier.go 🔗

@@ -22,6 +22,7 @@ type Querier interface {
 	GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
 	GetFileRead(ctx context.Context, arg GetFileReadParams) (ReadFile, error)
 	GetHourDayHeatmap(ctx context.Context) ([]GetHourDayHeatmapRow, error)
+	GetLastSession(ctx context.Context) (Session, error)
 	GetMessage(ctx context.Context, id string) (Message, error)
 	GetRecentActivity(ctx context.Context) ([]GetRecentActivityRow, error)
 	GetSessionByID(ctx context.Context, id string) (Session, error)

internal/db/sessions.sql.go 🔗

@@ -83,6 +83,32 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error {
 	return err
 }
 
+const getLastSession = `-- name: GetLastSession :one
+SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
+FROM sessions
+ORDER BY updated_at DESC
+LIMIT 1
+`
+
+func (q *Queries) GetLastSession(ctx context.Context) (Session, error) {
+	row := q.queryRow(ctx, q.getLastSessionStmt, getLastSession)
+	var i Session
+	err := row.Scan(
+		&i.ID,
+		&i.ParentSessionID,
+		&i.Title,
+		&i.MessageCount,
+		&i.PromptTokens,
+		&i.CompletionTokens,
+		&i.Cost,
+		&i.UpdatedAt,
+		&i.CreatedAt,
+		&i.SummaryMessageID,
+		&i.Todos,
+	)
+	return i, err
+}
+
 const getSessionByID = `-- name: GetSessionByID :one
 SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos
 FROM sessions

internal/db/sql/sessions.sql 🔗

@@ -28,6 +28,12 @@ SELECT *
 FROM sessions
 WHERE id = ? LIMIT 1;
 
+-- name: GetLastSession :one
+SELECT *
+FROM sessions
+ORDER BY updated_at DESC
+LIMIT 1;
+
 -- name: ListSessions :many
 SELECT *
 FROM sessions

internal/event/all.go 🔗

@@ -61,3 +61,23 @@ func TokensUsed(props ...any) {
 func StatsViewed() {
 	send("stats viewed")
 }
+
+func SessionListed(json bool) {
+	send("session listed", "json", json)
+}
+
+func SessionShown(json bool) {
+	send("session shown", "json", json)
+}
+
+func SessionLastShown(json bool) {
+	send("session last shown", "json", json)
+}
+
+func SessionDeletedCommand(json bool) {
+	send("session deleted", "json", json)
+}
+
+func SessionRenamed(json bool) {
+	send("session renamed", "json", json)
+}

internal/event/event.go 🔗

@@ -17,7 +17,9 @@ const (
 	endpoint = "https://data.charm.land"
 	key      = "phc_4zt4VgDWLqbYnJYEwLRxFoaTL2noNrQij0C6E8k3I0V"
 
-	nonInteractiveEventName = "NonInteractive"
+	nonInteractiveAttrName      = "NonInteractive"
+	continueSessionByIDAttrName = "ContinueSessionByID"
+	continueLastSessionAttrName = "ContinueLastSession"
 )
 
 var (
@@ -30,11 +32,19 @@ var (
 			Set("SHELL", filepath.Base(os.Getenv("SHELL"))).
 			Set("Version", version.Version).
 			Set("GoVersion", runtime.Version()).
-			Set(nonInteractiveEventName, false)
+			Set(nonInteractiveAttrName, false)
 )
 
 func SetNonInteractive(nonInteractive bool) {
-	baseProps = baseProps.Set(nonInteractiveEventName, nonInteractive)
+	baseProps = baseProps.Set(nonInteractiveAttrName, nonInteractive)
+}
+
+func SetContinueBySessionID(continueBySessionID bool) {
+	baseProps = baseProps.Set(continueSessionByIDAttrName, continueBySessionID)
+}
+
+func SetContinueLastSession(continueLastSession bool) {
+	baseProps = baseProps.Set(continueLastSessionAttrName, continueLastSession)
 }
 
 func Init() {

internal/session/session.go 🔗

@@ -66,6 +66,7 @@ type Service interface {
 	CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
 	CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
 	Get(ctx context.Context, id string) (Session, error)
+	GetLast(ctx context.Context) (Session, error)
 	List(ctx context.Context) ([]Session, error)
 	Save(ctx context.Context, session Session) (Session, error)
 	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) {
 	return s.fromDBItem(dbSession), nil
 }
 
+func (s *service) GetLast(ctx context.Context) (Session, error) {
+	dbSession, err := s.q.GetLastSession(ctx)
+	if err != nil {
+		return Session{}, err
+	}
+	return s.fromDBItem(dbSession), nil
+}
+
 func (s *service) Save(ctx context.Context, session Session) (Session, error) {
 	todosJSON, err := marshalTodos(session.Todos)
 	if err != nil {

internal/ui/chat/agent.go 🔗

@@ -101,7 +101,7 @@ type AgentToolRenderContext struct {
 func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
-		return pendingTool(sty, "Agent", opts.Anim)
+		return pendingTool(sty, "Agent", opts.Anim, opts.Compact)
 	}
 
 	var params agent.AgentParams
@@ -232,7 +232,7 @@ type agenticFetchParams struct {
 func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
-		return pendingTool(sty, "Agentic Fetch", opts.Anim)
+		return pendingTool(sty, "Agentic Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params agenticFetchParams

internal/ui/chat/bash.go 🔗

@@ -41,7 +41,7 @@ type BashToolRenderContext struct{}
 func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Bash", opts.Anim)
+		return pendingTool(sty, "Bash", opts.Anim, opts.Compact)
 	}
 
 	var params tools.BashParams
@@ -123,7 +123,7 @@ type JobOutputToolRenderContext struct{}
 func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Job", opts.Anim)
+		return pendingTool(sty, "Job", opts.Anim, opts.Compact)
 	}
 
 	var params tools.JobOutputParams
@@ -174,7 +174,7 @@ type JobKillToolRenderContext struct{}
 func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Job", opts.Anim)
+		return pendingTool(sty, "Job", opts.Anim, opts.Compact)
 	}
 
 	var params tools.JobKillParams

internal/ui/chat/diagnostics.go 🔗

@@ -37,7 +37,7 @@ type DiagnosticsToolRenderContext struct{}
 func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Diagnostics", opts.Anim)
+		return pendingTool(sty, "Diagnostics", opts.Anim, opts.Compact)
 	}
 
 	var params tools.DiagnosticsParams

internal/ui/chat/fetch.go 🔗

@@ -36,7 +36,7 @@ type FetchToolRenderContext struct{}
 func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Fetch", opts.Anim)
+		return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params tools.FetchParams
@@ -111,7 +111,7 @@ type WebFetchToolRenderContext struct{}
 func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Fetch", opts.Anim)
+		return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WebFetchParams
@@ -165,7 +165,7 @@ type WebSearchToolRenderContext struct{}
 func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Search", opts.Anim)
+		return pendingTool(sty, "Search", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WebSearchParams

internal/ui/chat/file.go 🔗

@@ -39,7 +39,7 @@ type ViewToolRenderContext struct{}
 func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "View", opts.Anim)
+		return pendingTool(sty, "View", opts.Anim, opts.Compact)
 	}
 
 	var params tools.ViewParams
@@ -125,7 +125,7 @@ type WriteToolRenderContext struct{}
 func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Write", opts.Anim)
+		return pendingTool(sty, "Write", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WriteParams
@@ -180,7 +180,7 @@ type EditToolRenderContext struct{}
 func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	// Edit tool uses full width for diffs.
 	if opts.IsPending() {
-		return pendingTool(sty, "Edit", opts.Anim)
+		return pendingTool(sty, "Edit", opts.Anim, opts.Compact)
 	}
 
 	var params tools.EditParams
@@ -243,7 +243,7 @@ type MultiEditToolRenderContext struct{}
 func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	// MultiEdit tool uses full width for diffs.
 	if opts.IsPending() {
-		return pendingTool(sty, "Multi-Edit", opts.Anim)
+		return pendingTool(sty, "Multi-Edit", opts.Anim, opts.Compact)
 	}
 
 	var params tools.MultiEditParams
@@ -311,7 +311,7 @@ type DownloadToolRenderContext struct{}
 func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Download", opts.Anim)
+		return pendingTool(sty, "Download", opts.Anim, opts.Compact)
 	}
 
 	var params tools.DownloadParams

internal/ui/chat/generic.go 🔗

@@ -35,7 +35,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
 	name := genericPrettyName(opts.ToolCall.Name)
 
 	if opts.IsPending() {
-		return pendingTool(sty, name, opts.Anim)
+		return pendingTool(sty, name, opts.Anim, opts.Compact)
 	}
 
 	var params map[string]any

internal/ui/chat/lsp_restart.go 🔗

@@ -32,7 +32,7 @@ type LSPRestartToolRenderContext struct{}
 func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Restart LSP", opts.Anim)
+		return pendingTool(sty, "Restart LSP", opts.Anim, opts.Compact)
 	}
 
 	var params tools.LSPRestartParams

internal/ui/chat/mcp.go 🔗

@@ -46,7 +46,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
 	name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
 
 	if opts.IsPending() {
-		return pendingTool(sty, name, opts.Anim)
+		return pendingTool(sty, name, opts.Anim, opts.Compact)
 	}
 
 	var params map[string]any

internal/ui/chat/references.go 🔗

@@ -33,7 +33,7 @@ type ReferencesToolRenderContext struct{}
 func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Find References", opts.Anim)
+		return pendingTool(sty, "Find References", opts.Anim, opts.Compact)
 	}
 
 	var params tools.ReferencesParams

internal/ui/chat/search.go 🔗

@@ -37,7 +37,7 @@ type GlobToolRenderContext struct{}
 func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Glob", opts.Anim)
+		return pendingTool(sty, "Glob", opts.Anim, opts.Compact)
 	}
 
 	var params tools.GlobParams
@@ -96,7 +96,7 @@ type GrepToolRenderContext struct{}
 func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Grep", opts.Anim)
+		return pendingTool(sty, "Grep", opts.Anim, opts.Compact)
 	}
 
 	var params tools.GrepParams
@@ -161,7 +161,7 @@ type LSToolRenderContext struct{}
 func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "List", opts.Anim)
+		return pendingTool(sty, "List", opts.Anim, opts.Compact)
 	}
 
 	var params tools.LSParams
@@ -221,7 +221,7 @@ type SourcegraphToolRenderContext struct{}
 func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Sourcegraph", opts.Anim)
+		return pendingTool(sty, "Sourcegraph", opts.Anim, opts.Compact)
 	}
 
 	var params tools.SourcegraphParams

internal/ui/chat/todos.go 🔗

@@ -41,7 +41,7 @@ type TodosToolRenderContext struct{}
 func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "To-Do", opts.Anim)
+		return pendingTool(sty, "To-Do", opts.Anim, opts.Compact)
 	}
 
 	var params tools.TodosParams

internal/ui/chat/tools.go 🔗

@@ -424,9 +424,13 @@ func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
 }
 
 // pendingTool renders a tool that is still in progress with an animation.
-func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
+func pendingTool(sty *styles.Styles, name string, anim *anim.Anim, nested bool) string {
 	icon := sty.Tool.IconPending.Render()
-	toolName := sty.Tool.NameNormal.Render(name)
+	nameStyle := sty.Tool.NameNormal
+	if nested {
+		nameStyle = sty.Tool.NameNested
+	}
+	toolName := nameStyle.Render(name)
 
 	var animView string
 	if anim != nil {

internal/ui/dialog/models_list.go 🔗

@@ -216,8 +216,10 @@ func (f *ModelsList) VisibleItems() []list.Item {
 		}
 
 		matches := fuzzy.Find(query, names)
+
+		// Sort by original index to preserve order within the group
 		sort.SliceStable(matches, func(i, j int) bool {
-			return matches[i].Score > matches[j].Score
+			return matches[i].Index < matches[j].Index
 		})
 
 		for _, match := range matches {

internal/ui/styles/styles.go 🔗

@@ -254,18 +254,18 @@ type Styles struct {
 	// Tool - styles for tool call rendering
 	Tool struct {
 		// Icon styles with tool status
-		IconPending   lipgloss.Style // Pending operation icon
-		IconSuccess   lipgloss.Style // Successful operation icon
-		IconError     lipgloss.Style // Error operation icon
-		IconCancelled lipgloss.Style // Cancelled operation icon
+		IconPending   lipgloss.Style
+		IconSuccess   lipgloss.Style
+		IconError     lipgloss.Style
+		IconCancelled lipgloss.Style
 
 		// Tool name styles
-		NameNormal lipgloss.Style // Normal tool name
-		NameNested lipgloss.Style // Nested tool name
+		NameNormal lipgloss.Style // Top-level tool name
+		NameNested lipgloss.Style // Nested child tool name (inside Agent/Agentic Fetch)
 
 		// Parameter list styles
-		ParamMain lipgloss.Style // Main parameter
-		ParamKey  lipgloss.Style // Parameter keys
+		ParamMain lipgloss.Style
+		ParamKey  lipgloss.Style
 
 		// Content rendering styles
 		ContentLine           lipgloss.Style // Individual content line with background and width
@@ -632,14 +632,14 @@ func DefaultStyles() Styles {
 			StylePrimitive: ansi.StylePrimitive{
 				// BlockPrefix: "\n",
 				// BlockSuffix: "\n",
-				Color: stringPtr(charmtone.Smoke.Hex()),
+				Color: new(charmtone.Smoke.Hex()),
 			},
-			// Margin: uintPtr(defaultMargin),
+			// Margin: new(uint(defaultMargin)),
 		},
 		BlockQuote: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{},
-			Indent:         uintPtr(1),
-			IndentToken:    stringPtr("│ "),
+			Indent:         new(uint(1)),
+			IndentToken:    new("│ "),
 		},
 		List: ansi.StyleList{
 			LevelIndent: defaultListIndent,
@@ -647,17 +647,17 @@ func DefaultStyles() Styles {
 		Heading: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				BlockSuffix: "\n",
-				Color:       stringPtr(charmtone.Malibu.Hex()),
-				Bold:        boolPtr(true),
+				Color:       new(charmtone.Malibu.Hex()),
+				Bold:        new(true),
 			},
 		},
 		H1: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix:          " ",
 				Suffix:          " ",
-				Color:           stringPtr(charmtone.Zest.Hex()),
-				BackgroundColor: stringPtr(charmtone.Charple.Hex()),
-				Bold:            boolPtr(true),
+				Color:           new(charmtone.Zest.Hex()),
+				BackgroundColor: new(charmtone.Charple.Hex()),
+				Bold:            new(true),
 			},
 		},
 		H2: ansi.StyleBlock{
@@ -683,21 +683,21 @@ func DefaultStyles() Styles {
 		H6: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "###### ",
-				Color:  stringPtr(charmtone.Guac.Hex()),
-				Bold:   boolPtr(false),
+				Color:  new(charmtone.Guac.Hex()),
+				Bold:   new(false),
 			},
 		},
 		Strikethrough: ansi.StylePrimitive{
-			CrossedOut: boolPtr(true),
+			CrossedOut: new(true),
 		},
 		Emph: ansi.StylePrimitive{
-			Italic: boolPtr(true),
+			Italic: new(true),
 		},
 		Strong: ansi.StylePrimitive{
-			Bold: boolPtr(true),
+			Bold: new(true),
 		},
 		HorizontalRule: ansi.StylePrimitive{
-			Color:  stringPtr(charmtone.Charcoal.Hex()),
+			Color:  new(charmtone.Charcoal.Hex()),
 			Format: "\n--------\n",
 		},
 		Item: ansi.StylePrimitive{
@@ -712,117 +712,117 @@ func DefaultStyles() Styles {
 			Unticked:       "[ ] ",
 		},
 		Link: ansi.StylePrimitive{
-			Color:     stringPtr(charmtone.Zinc.Hex()),
-			Underline: boolPtr(true),
+			Color:     new(charmtone.Zinc.Hex()),
+			Underline: new(true),
 		},
 		LinkText: ansi.StylePrimitive{
-			Color: stringPtr(charmtone.Guac.Hex()),
-			Bold:  boolPtr(true),
+			Color: new(charmtone.Guac.Hex()),
+			Bold:  new(true),
 		},
 		Image: ansi.StylePrimitive{
-			Color:     stringPtr(charmtone.Cheeky.Hex()),
-			Underline: boolPtr(true),
+			Color:     new(charmtone.Cheeky.Hex()),
+			Underline: new(true),
 		},
 		ImageText: ansi.StylePrimitive{
-			Color:  stringPtr(charmtone.Squid.Hex()),
+			Color:  new(charmtone.Squid.Hex()),
 			Format: "Image: {{.text}} →",
 		},
 		Code: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix:          " ",
 				Suffix:          " ",
-				Color:           stringPtr(charmtone.Coral.Hex()),
-				BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
+				Color:           new(charmtone.Coral.Hex()),
+				BackgroundColor: new(charmtone.Charcoal.Hex()),
 			},
 		},
 		CodeBlock: ansi.StyleCodeBlock{
 			StyleBlock: ansi.StyleBlock{
 				StylePrimitive: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Charcoal.Hex()),
+					Color: new(charmtone.Charcoal.Hex()),
 				},
-				Margin: uintPtr(defaultMargin),
+				Margin: new(uint(defaultMargin)),
 			},
 			Chroma: &ansi.Chroma{
 				Text: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Smoke.Hex()),
+					Color: new(charmtone.Smoke.Hex()),
 				},
 				Error: ansi.StylePrimitive{
-					Color:           stringPtr(charmtone.Butter.Hex()),
-					BackgroundColor: stringPtr(charmtone.Sriracha.Hex()),
+					Color:           new(charmtone.Butter.Hex()),
+					BackgroundColor: new(charmtone.Sriracha.Hex()),
 				},
 				Comment: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Oyster.Hex()),
+					Color: new(charmtone.Oyster.Hex()),
 				},
 				CommentPreproc: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Bengal.Hex()),
+					Color: new(charmtone.Bengal.Hex()),
 				},
 				Keyword: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Malibu.Hex()),
+					Color: new(charmtone.Malibu.Hex()),
 				},
 				KeywordReserved: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Pony.Hex()),
+					Color: new(charmtone.Pony.Hex()),
 				},
 				KeywordNamespace: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Pony.Hex()),
+					Color: new(charmtone.Pony.Hex()),
 				},
 				KeywordType: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Guppy.Hex()),
+					Color: new(charmtone.Guppy.Hex()),
 				},
 				Operator: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Salmon.Hex()),
+					Color: new(charmtone.Salmon.Hex()),
 				},
 				Punctuation: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Zest.Hex()),
+					Color: new(charmtone.Zest.Hex()),
 				},
 				Name: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Smoke.Hex()),
+					Color: new(charmtone.Smoke.Hex()),
 				},
 				NameBuiltin: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Cheeky.Hex()),
+					Color: new(charmtone.Cheeky.Hex()),
 				},
 				NameTag: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Mauve.Hex()),
+					Color: new(charmtone.Mauve.Hex()),
 				},
 				NameAttribute: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Hazy.Hex()),
+					Color: new(charmtone.Hazy.Hex()),
 				},
 				NameClass: ansi.StylePrimitive{
-					Color:     stringPtr(charmtone.Salt.Hex()),
-					Underline: boolPtr(true),
-					Bold:      boolPtr(true),
+					Color:     new(charmtone.Salt.Hex()),
+					Underline: new(true),
+					Bold:      new(true),
 				},
 				NameDecorator: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Citron.Hex()),
+					Color: new(charmtone.Citron.Hex()),
 				},
 				NameFunction: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Guac.Hex()),
+					Color: new(charmtone.Guac.Hex()),
 				},
 				LiteralNumber: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Julep.Hex()),
+					Color: new(charmtone.Julep.Hex()),
 				},
 				LiteralString: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Cumin.Hex()),
+					Color: new(charmtone.Cumin.Hex()),
 				},
 				LiteralStringEscape: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Bok.Hex()),
+					Color: new(charmtone.Bok.Hex()),
 				},
 				GenericDeleted: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Coral.Hex()),
+					Color: new(charmtone.Coral.Hex()),
 				},
 				GenericEmph: ansi.StylePrimitive{
-					Italic: boolPtr(true),
+					Italic: new(true),
 				},
 				GenericInserted: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Guac.Hex()),
+					Color: new(charmtone.Guac.Hex()),
 				},
 				GenericStrong: ansi.StylePrimitive{
-					Bold: boolPtr(true),
+					Bold: new(true),
 				},
 				GenericSubheading: ansi.StylePrimitive{
-					Color: stringPtr(charmtone.Squid.Hex()),
+					Color: new(charmtone.Squid.Hex()),
 				},
 				Background: ansi.StylePrimitive{
-					BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
+					BackgroundColor: new(charmtone.Charcoal.Hex()),
 				},
 			},
 		},
@@ -837,8 +837,8 @@ func DefaultStyles() Styles {
 	}
 
 	// PlainMarkdown style - muted colors on subtle background for thinking content.
-	plainBg := stringPtr(bgBaseLighter.Hex())
-	plainFg := stringPtr(fgMuted.Hex())
+	plainBg := new(bgBaseLighter.Hex())
+	plainFg := new(fgMuted.Hex())
 	s.PlainMarkdown = ansi.StyleConfig{
 		Document: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
@@ -851,8 +851,8 @@ func DefaultStyles() Styles {
 				Color:           plainFg,
 				BackgroundColor: plainBg,
 			},
-			Indent:      uintPtr(1),
-			IndentToken: stringPtr("│ "),
+			Indent:      new(uint(1)),
+			IndentToken: new("│ "),
 		},
 		List: ansi.StyleList{
 			LevelIndent: defaultListIndent,
@@ -860,7 +860,7 @@ func DefaultStyles() Styles {
 		Heading: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				BlockSuffix:     "\n",
-				Bold:            boolPtr(true),
+				Bold:            new(true),
 				Color:           plainFg,
 				BackgroundColor: plainBg,
 			},
@@ -869,7 +869,7 @@ func DefaultStyles() Styles {
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix:          " ",
 				Suffix:          " ",
-				Bold:            boolPtr(true),
+				Bold:            new(true),
 				Color:           plainFg,
 				BackgroundColor: plainBg,
 			},
@@ -910,17 +910,17 @@ func DefaultStyles() Styles {
 			},
 		},
 		Strikethrough: ansi.StylePrimitive{
-			CrossedOut:      boolPtr(true),
+			CrossedOut:      new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
 		Emph: ansi.StylePrimitive{
-			Italic:          boolPtr(true),
+			Italic:          new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
 		Strong: ansi.StylePrimitive{
-			Bold:            boolPtr(true),
+			Bold:            new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
@@ -948,17 +948,17 @@ func DefaultStyles() Styles {
 			Unticked: "[ ] ",
 		},
 		Link: ansi.StylePrimitive{
-			Underline:       boolPtr(true),
+			Underline:       new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
 		LinkText: ansi.StylePrimitive{
-			Bold:            boolPtr(true),
+			Bold:            new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
 		Image: ansi.StylePrimitive{
-			Underline:       boolPtr(true),
+			Underline:       new(true),
 			Color:           plainFg,
 			BackgroundColor: plainBg,
 		},
@@ -981,7 +981,7 @@ func DefaultStyles() Styles {
 					Color:           plainFg,
 					BackgroundColor: plainBg,
 				},
-				Margin: uintPtr(defaultMargin),
+				Margin: new(uint(defaultMargin)),
 			},
 		},
 		Table: ansi.StyleTable{
@@ -1119,7 +1119,7 @@ func DefaultStyles() Styles {
 	s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
 
 	s.Tool.NameNormal = base.Foreground(blue)
-	s.Tool.NameNested = base.Foreground(fgHalfMuted)
+	s.Tool.NameNested = base.Foreground(blue)
 
 	s.Tool.ParamMain = s.Subtle
 	s.Tool.ParamKey = s.Subtle
@@ -1368,10 +1368,6 @@ func DefaultStyles() Styles {
 	return s
 }
 
-// Helper functions for style pointers
-func boolPtr(b bool) *bool       { return &b }
-func stringPtr(s string) *string { return &s }
-func uintPtr(u uint) *uint       { return &u }
 func chromaStyle(style ansi.StylePrimitive) string {
 	var s strings.Builder