Detailed changes
@@ -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
@@ -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
@@ -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
@@ -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=
@@ -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.
@@ -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")
+}
@@ -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))
+ }
+}
@@ -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
@@ -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)
@@ -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,
@@ -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)
@@ -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
@@ -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
@@ -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)
+}
@@ -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() {
@@ -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 {
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
@@ -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 {
@@ -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