loop: do slug generation outside the agent loop

[This commit message written entirely by a human; it is all useful.]

We can make a slug based on the first message.
It's good enough.
That keeps it--and the slug tool--out of the agent's context.
It's also one fewer step for extremely short Sketch runs,
which is the straw that broke this particular camel's back.

This is a mild UI regression, in that there's a slight stall
after the user types their first message, during which
the slug is being generated. See (2) below.

While we're here, add handling of compaction agent messages.

This leaves two big TODOs outstanding:

1.

Untangle the awful rats nest that is slug and branch management;
we have callbacks back and forth and layers and locking and it's all confusing.
One visible for that this ugliness takes is that every time the agent tries out a slug,
the top bar in the webui updates, even if we then reject that slug as a duplicate.
there are other forms of ugliness, just less visible.

2.

Make slug generation concurrent with the rest of the agent,
to avoid a short stall right after the user's first request (ick).

When we make slug setting concurrent, we'll likely need to resuscitate
the bashPermissionCheck, except it'll be "silently block and wait for
background slug generation to complete", rather than "reject the tool call".
Ditto for about_sketch, and any other tool call that expects
the slug or branch name to be set.

Generally, before undertaking this effort, we should fix (1) above,
make convos generally concurrency safe (maybe COW?), and
figure out to get race-enabled innie builds.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8ac5f6a9faa611ebk
diff --git a/cmd/go2ts/go2ts.go b/cmd/go2ts/go2ts.go
index e05a7ea..6025026 100644
--- a/cmd/go2ts/go2ts.go
+++ b/cmd/go2ts/go2ts.go
@@ -48,6 +48,8 @@
 			loop.CommitMessageType,
 			loop.AutoMessageType,
 			loop.PortMessageType,
+			loop.CompactMessageType,
+			loop.SlugMessageType,
 		},
 	)
 
diff --git a/loop/agent.go b/loop/agent.go
index 9927d68..40bbb20 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -21,7 +21,6 @@
 
 	"sketch.dev/browser"
 	"sketch.dev/claudetool"
-	"sketch.dev/claudetool/bashkit"
 	"sketch.dev/claudetool/browse"
 	"sketch.dev/claudetool/codereview"
 	"sketch.dev/claudetool/onstart"
@@ -179,6 +178,7 @@
 	AutoMessageType    CodingAgentMessageType = "auto"    // for automated notifications like autoformatting
 	CompactMessageType CodingAgentMessageType = "compact" // for conversation compaction notifications
 	PortMessageType    CodingAgentMessageType = "port"    // for port monitoring events
+	SlugMessageType    CodingAgentMessageType = "slug"    // for slug updates
 
 	cancelToolUseMessage = "Stop responding to my previous message. Wait for me to ask you something else before attempting to use any more tools."
 )
@@ -1350,23 +1350,7 @@
 	convo.SystemPrompt = a.renderSystemPrompt()
 	convo.ExtraData = map[string]any{"session_id": a.config.SessionID}
 
-	// Define a permission callback for the bash tool to check if the branch name is set before allowing git commits
-	bashPermissionCheck := func(command string) error {
-		if a.gitState.Slug() != "" {
-			return nil // branch is set up
-		}
-		willCommit, err := bashkit.WillRunGitCommit(command)
-		if err != nil {
-			return nil // fail open
-		}
-		if willCommit {
-			return fmt.Errorf("you must use the set-slug tool before making git commits")
-		}
-		return nil
-	}
-
 	bashTool := &claudetool.BashTool{
-		CheckPermission:  bashPermissionCheck,
 		EnableJITInstall: claudetool.EnableBashToolJITInstall,
 		Timeouts:         a.config.BashTimeouts,
 	}
@@ -1390,7 +1374,7 @@
 
 	convo.Tools = []*llm.Tool{
 		bashTool.Tool(), claudetool.Keyword, claudetool.Patch(a.patchCallback),
-		claudetool.Think, claudetool.TodoRead, claudetool.TodoWrite, a.setSlugTool(), a.commitMessageStyleTool(), makeDoneTool(a.codereview),
+		claudetool.Think, claudetool.TodoRead, claudetool.TodoWrite, a.commitMessageStyleTool(), makeDoneTool(a.codereview),
 		a.codereview.Tool(), claudetool.AboutSketch,
 	}
 
@@ -1519,47 +1503,102 @@
 	return false
 }
 
-func (a *Agent) setSlugTool() *llm.Tool {
-	return &llm.Tool{
-		Name:        "set-slug",
-		Description: `Set a short slug as an identifier for this conversation.`,
-		InputSchema: json.RawMessage(`{
-	"type": "object",
-	"properties": {
-		"slug": {
-			"type": "string",
-			"description": "A 2-3 word alphanumeric hyphenated slug, imperative tense"
+func soleText(contents []llm.Content) (string, error) {
+	if len(contents) != 1 {
+		return "", fmt.Errorf("multiple contents %v", contents)
+	}
+	content := contents[0]
+	if content.Type != llm.ContentTypeText || content.Text == "" {
+		return "", fmt.Errorf("bad content %v", content)
+	}
+	return strings.TrimSpace(content.Text), nil
+}
+
+// autoGenerateSlug automatically generates a slug based on the first user input
+func (a *Agent) autoGenerateSlug(ctx context.Context, userContents []llm.Content) error {
+	userText, err := soleText(userContents)
+	if err != nil {
+		return err
+	}
+	if userText == "" {
+		return fmt.Errorf("set-slug: empty text content")
+	}
+
+	// Create a subconversation without history for slug generation
+	convo, ok := a.convo.(*conversation.Convo)
+	if !ok {
+		// In test environments, the conversation might be a mock interface
+		// Skip slug generation in this case
+		return fmt.Errorf("set-slug: can't make a subconvo (mock convo?)")
+	}
+
+	// Loop until we find an acceptable slug
+	var unavailableSlugs []string
+	for {
+		if len(unavailableSlugs) > 10 {
+			// sanity check to prevent infinite loops
+			return fmt.Errorf("set-slug: failed to construct a new slug after %d attempts", len(unavailableSlugs))
 		}
-	},
-	"required": ["slug"]
-}`),
-		Run: func(ctx context.Context, input json.RawMessage) llm.ToolOut {
-			var params struct {
-				Slug string `json:"slug"`
-			}
-			if err := json.Unmarshal(input, &params); err != nil {
-				return llm.ErrorToolOut(err)
-			}
-			// Prevent slug changes if there have been git changes
-			// This lets the agent change its mind about a good slug,
-			// while ensuring that once a branch has been pushed, it remains stable.
-			if s := a.Slug(); s != "" && s != params.Slug && a.gitState.HasSeenCommits() {
-				return llm.ErrorfToolOut("slug already set to %q", s)
-			}
-			if params.Slug == "" {
-				return llm.ErrorToolOut(fmt.Errorf("slug parameter cannot be empty"))
-			}
-			slug := cleanSlugName(params.Slug)
-			if slug == "" {
-				return llm.ErrorToolOut(fmt.Errorf("slug parameter could not be converted to a valid slug"))
-			}
-			a.SetSlug(slug)
-			// TODO: do this by a call to outie, rather than semi-guessing from innie
-			if branchExists(a.workingDir, a.BranchName()) {
-				return llm.ErrorfToolOut("slug %q already exists; please choose a different slug", slug)
-			}
-			return llm.ToolOut{LLMContent: llm.TextContent("OK")}
-		},
+		subConvo := convo.SubConvo()
+		subConvo.Hidden = true
+
+		// Prompt for slug generation
+		prompt := `You are a slug generator for Sketch, an agentic coding environment.
+The user's prompt will be in <user-prompt> tags. Any unavailable slugs will be listed in <unavailable-slug> tags.
+Generate a 2-3 word alphanumeric hyphenated slug in imperative tense that captures the essence of their coding task.
+Respond with only the slug.`
+
+		buf := new(strings.Builder)
+		buf.WriteString("<slug-request>")
+		if len(unavailableSlugs) > 0 {
+			buf.WriteString("<unavailable-slugs>")
+		}
+		for _, x := range unavailableSlugs {
+			buf.WriteString("<unavailable-slug>")
+			buf.WriteString(x)
+			buf.WriteString("</unavailable-slug>")
+		}
+		if len(unavailableSlugs) > 0 {
+			buf.WriteString("</unavailable-slugs>")
+		}
+		buf.WriteString("<user-prompt>")
+		buf.WriteString(userText)
+		buf.WriteString("</user-prompt>")
+		buf.WriteString("</slug-request>")
+
+		fullPrompt := prompt + "\n" + buf.String()
+		userMessage := llm.UserStringMessage(fullPrompt)
+
+		resp, err := subConvo.SendMessage(userMessage)
+		if err != nil {
+			return fmt.Errorf("failed to generate slug: %w", err)
+		}
+
+		// Extract the slug from the response
+		slugText, err := soleText(resp.Content)
+		if err != nil {
+			return err
+		}
+		if slugText == "" {
+			return fmt.Errorf("empty slug generated")
+		}
+
+		// Clean and validate the slug
+		slug := cleanSlugName(slugText)
+		if slug == "" {
+			return fmt.Errorf("slug could not be cleaned: %q", slugText)
+		}
+
+		// Check if branch already exists using the same logic as the original set-slug tool
+		a.SetSlug(slug) // Set slug first so BranchName() works correctly
+		if branchExists(a.workingDir, a.BranchName()) {
+			// try again
+			unavailableSlugs = append(unavailableSlugs, slug)
+			continue
+		}
+
+		// Success! Slug is available and already set
+		return nil
 	}
 }
 
@@ -1802,6 +1841,23 @@
 		return nil, err
 	}
 
+	// Auto-generate slug if this is the first user input and no slug is set
+	if a.Slug() == "" {
+		if err := a.autoGenerateSlug(ctx, msgs); err != nil {
+			// NB: it is possible that autoGenerateSlug set the slug during the process
+			// of trying to generate a slug.
+			// The fact that it returned an error means that we cannot use that slug.
+			slog.WarnContext(ctx, "Failed to auto-generate slug", "error", err)
+			// use the session id instead. ugly, but we need a slug, and this will be unique.
+			a.SetSlug(a.SessionID())
+		}
+		// Notify termui of the final slug (only emitted once, after slug is determined)
+		a.pushToOutbox(ctx, AgentMessage{
+			Type:    SlugMessageType,
+			Content: a.Slug(),
+		})
+	}
+
 	userMessage := llm.Message{
 		Role:    llm.MessageRoleUser,
 		Content: msgs,
diff --git a/loop/agent_system_prompt.txt b/loop/agent_system_prompt.txt
index dbf4357..2d60f72 100644
--- a/loop/agent_system_prompt.txt
+++ b/loop/agent_system_prompt.txt
@@ -13,8 +13,6 @@
 Aim for a small diff size while thoroughly completing the requested task.
 Prioritize thoughtful analysis and critical engagement over agreeability.
 
-Call the set-slug tool as soon as the topic of conversation is clear, often immediately.
-
 Break down the overall goal into a series of smaller steps.
 Use the todo_read and todo_write tools to organize and track your work systematically.
 
diff --git a/loop/agent_test.go b/loop/agent_test.go
index 0d0e6ab..38422a3 100644
--- a/loop/agent_test.go
+++ b/loop/agent_test.go
@@ -91,7 +91,10 @@
 	}
 
 	// Setup a test message that will trigger a simple, predictable response
-	userMessage := "What tools are available to you? Please just list them briefly. (Do not call the set-slug tool.)"
+	userMessage := "What tools are available to you? Please just list them briefly."
+
+	// Set a slug so that the agent doesn't have to.
+	agent.SetSlug("list-available-tools")
 
 	// Send the message to the agent
 	agent.UserMessage(ctx, userMessage)
@@ -239,16 +242,20 @@
 	agent.mu.Lock()
 	defer agent.mu.Unlock()
 
-	// There should be exactly one message
-	if len(agent.history) != 1 {
-		t.Errorf("Expected exactly one message, got %d", len(agent.history))
+	// There should be exactly two messages: slug + error
+	if len(agent.history) != 2 {
+		t.Errorf("Expected exactly two messages (slug + error), got %d", len(agent.history))
 	} else {
-		msg := agent.history[0]
-		if msg.Type != ErrorMessageType {
-			t.Errorf("Expected error message, got message type: %s", msg.Type)
+		slugMsg := agent.history[0]
+		if slugMsg.Type != SlugMessageType {
+			t.Errorf("Expected first message to be slug, got message type: %s", slugMsg.Type)
 		}
-		if !strings.Contains(msg.Content, "simulating nil response") {
-			t.Errorf("Expected error message to contain 'simulating nil response', got: %s", msg.Content)
+		errorMsg := agent.history[1]
+		if errorMsg.Type != ErrorMessageType {
+			t.Errorf("Expected second message to be error, got message type: %s", errorMsg.Type)
+		}
+		if !strings.Contains(errorMsg.Content, "simulating nil response") {
+			t.Errorf("Expected error message to contain 'simulating nil response', got: %s", errorMsg.Content)
 		}
 	}
 }
@@ -395,16 +402,20 @@
 	agent.mu.Lock()
 	defer agent.mu.Unlock()
 
-	// There should be exactly one message
-	if len(agent.history) != 1 {
-		t.Errorf("Expected exactly one message, got %d", len(agent.history))
+	// There should be exactly two messages: slug + error
+	if len(agent.history) != 2 {
+		t.Errorf("Expected exactly two messages (slug + error), got %d", len(agent.history))
 	} else {
-		msg := agent.history[0]
-		if msg.Type != ErrorMessageType {
-			t.Errorf("Expected error message type, got: %s", msg.Type)
+		slugMsg := agent.history[0]
+		if slugMsg.Type != SlugMessageType {
+			t.Errorf("Expected first message to be slug, got message type: %s", slugMsg.Type)
 		}
-		if !strings.Contains(msg.Content, "unexpected nil response") {
-			t.Errorf("Expected error about nil response, got: %s", msg.Content)
+		errorMsg := agent.history[1]
+		if errorMsg.Type != ErrorMessageType {
+			t.Errorf("Expected second message to be error, got message type: %s", errorMsg.Type)
+		}
+		if !strings.Contains(errorMsg.Content, "unexpected nil response") {
+			t.Errorf("Expected error about nil response, got: %s", errorMsg.Content)
 		}
 	}
 }
@@ -745,3 +756,133 @@
 		t.Errorf("Expected Content to be %q, got %q", expected, received.Content)
 	}
 }
+
+// TestCleanSlugName tests the slug cleaning function
+func TestCleanSlugName(t *testing.T) {
+	tests := []struct {
+		name  string
+		input string
+		want  string
+	}{
+		{"simple lowercase", "fix-bug", "fix-bug"},
+		{"uppercase to lowercase", "FIX-BUG", "fix-bug"},
+		{"spaces to hyphens", "fix login bug", "fix-login-bug"},
+		{"mixed case and spaces", "Fix Login Bug", "fix-login-bug"},
+		{"special characters removed", "fix_bug@home!", "fixbughome"},
+		{"multiple hyphens preserved", "fix--bug---here", "fix--bug---here"},
+		{"leading/trailing hyphens preserved", "-fix-bug-", "-fix-bug-"},
+		{"numbers preserved", "fix-bug-v2", "fix-bug-v2"},
+		{"empty string", "", ""},
+		{"only special chars", "@#$%", ""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := cleanSlugName(tt.input)
+			if got != tt.want {
+				t.Errorf("cleanSlugName(%q) = %q, want %q", tt.input, got, tt.want)
+			}
+		})
+	}
+}
+
+// TestAutoGenerateSlugInputValidation tests input validation for auto slug generation
+func TestAutoGenerateSlugInputValidation(t *testing.T) {
+	// Test soleText with empty input
+	emptyContents := []llm.Content{}
+	_, err := soleText(emptyContents)
+	if err == nil {
+		t.Errorf("Expected error for empty contents, got nil")
+	}
+
+	// Test with non-text content only
+	nonTextContents := []llm.Content{
+		{Type: llm.ContentTypeToolUse, ToolName: "bash"},
+	}
+	_, err = soleText(nonTextContents)
+	if err == nil {
+		t.Errorf("Expected error for non-text contents, got nil")
+	}
+
+	// Test slug formatting
+	testInputs := []string{
+		"Fix the login bug",
+		"Add user authentication system",
+		"Refactor API endpoints",
+		"Update documentation",
+	}
+
+	for _, input := range testInputs {
+		slug := cleanSlugName(strings.ToLower(strings.ReplaceAll(input, " ", "-")))
+		if slug == "" {
+			t.Errorf("cleanSlugName produced empty result for input %q", input)
+		}
+		if !strings.Contains(slug, "-") {
+			// We expect most multi-word inputs to contain hyphens after processing
+			t.Logf("Input %q produced slug %q (no hyphen found, might be single word)", input, slug)
+		}
+	}
+}
+
+// TestSoleText tests the soleText helper function
+func TestSoleText(t *testing.T) {
+	tests := []struct {
+		name     string
+		contents []llm.Content
+		wantText string
+		wantErr  bool
+	}{
+		{
+			name: "single text content",
+			contents: []llm.Content{
+				{Type: llm.ContentTypeText, Text: "  Hello world  "},
+			},
+			wantText: "Hello world",
+			wantErr:  false,
+		},
+		{
+			name:     "empty slice",
+			contents: []llm.Content{},
+			wantText: "",
+			wantErr:  true,
+		},
+		{
+			name: "multiple contents",
+			contents: []llm.Content{
+				{Type: llm.ContentTypeText, Text: "First"},
+				{Type: llm.ContentTypeText, Text: "Second"},
+			},
+			wantText: "",
+			wantErr:  true,
+		},
+		{
+			name: "non-text content",
+			contents: []llm.Content{
+				{Type: llm.ContentTypeToolUse, ToolName: "bash"},
+			},
+			wantText: "",
+			wantErr:  true,
+		},
+		{
+			name: "empty text content",
+			contents: []llm.Content{
+				{Type: llm.ContentTypeText, Text: ""},
+			},
+			wantText: "",
+			wantErr:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotText, err := soleText(tt.contents)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("soleText() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if gotText != tt.wantText {
+				t.Errorf("soleText() gotText = %v, want %v", gotText, tt.wantText)
+			}
+		})
+	}
+}
diff --git a/loop/testdata/agent_loop.httprr b/loop/testdata/agent_loop.httprr
index 25aca32..b2ac507 100644
--- a/loop/testdata/agent_loop.httprr
+++ b/loop/testdata/agent_loop.httprr
@@ -1,9 +1,9 @@
 httprr trace v1
-20567 2526
+20094 2538
 POST https://api.anthropic.com/v1/messages HTTP/1.1

 Host: api.anthropic.com

 User-Agent: Go-http-client/1.1

-Content-Length: 20369

+Content-Length: 19896

 Anthropic-Version: 2023-06-01

 Content-Type: application/json

 

@@ -15,7 +15,7 @@
    "content": [
     {
      "type": "text",
-     "text": "What tools are available to you? Please just list them briefly. (Do not call the set-slug tool.)",
+     "text": "What tools are available to you? Please just list them briefly.",
      "cache_control": {
       "type": "ephemeral"
      }
@@ -189,22 +189,6 @@
    }
   },
   {
-   "name": "set-slug",
-   "description": "Set a short slug as an identifier for this conversation.",
-   "input_schema": {
-    "type": "object",
-    "properties": {
-     "slug": {
-      "type": "string",
-      "description": "A 2-3 word alphanumeric hyphenated slug, imperative tense"
-     }
-    },
-    "required": [
-     "slug"
-    ]
-   }
-  },
-  {
    "name": "commit-message-style",
    "description": "Provides git commit message style guidance. MANDATORY: You must use this tool before making any git commits.",
    "input_schema": {
@@ -592,7 +576,7 @@
  ],
  "system": [
   {
-   "text": "You are the expert software engineer and architect powering Sketch,\nan agentic coding environment that helps users accomplish coding tasks through autonomous analysis and implementation.\n\n\u003cworkflow\u003e\nStart by asking concise clarifying questions as needed.\nOnce the intent is clear, work autonomously.\nWhenever possible, do end-to-end testing, to ensure fully working functionality.\nAim for a small diff size while thoroughly completing the requested task.\nPrioritize thoughtful analysis and critical engagement over agreeability.\n\nCall the set-slug tool as soon as the topic of conversation is clear, often immediately.\n\nBreak down the overall goal into a series of smaller steps.\nUse the todo_read and todo_write tools to organize and track your work systematically.\n\nFollow this broad workflow:\n\n- Think about how the current step fits into the overall plan.\n- Do research. Good tool choices: bash, think, keyword_search\n- Make edits.\n- If you have completed a standalone chunk of work, make a git commit.\n- Update your todo task list.\n- Repeat.\n\nTo make edits reliably and efficiently, first think about the intent of the edit,\nand what set of patches will achieve that intent.\nThen use the patch tool to make those edits. Combine all edits to any given file into a single patch tool call.\n\nYou may run tool calls in parallel.\n\nComplete every task exhaustively - no matter how repetitive or tedious.\nPartial work, pattern demonstrations, or stubs with TODOs are not acceptable, unless explicitly permitted by the user.\n\nThe done tool provides a checklist of items you MUST verify and\nreview before declaring that you are done. Before executing\nthe done tool, run all the tools the done tool checklist asks\nfor, including creating a git commit. Do not forget to run tests.\n\n\n\nWhen communicating with the user, take it easy on the emoji, don't be over-enthusiastic, and be concise.\n\nDocker is available. Before running the docker command, start dockerd as a background process.\nAlways use --network=host when running docker containers.\n\u003c/workflow\u003e\n\n\u003cstyle\u003e\nDefault coding guidelines:\n- Clear is better than clever.\n- Minimal inline comments: non-obvious logic and key decisions only.\n\u003c/style\u003e\n\n\u003csystem_info\u003e\n\u003cplatform\u003e\nlinux/amd64\n\u003c/platform\u003e\n\u003cpwd\u003e\n/\n\u003c/pwd\u003e\n\u003c/system_info\u003e\n\n\u003cgit_info\u003e\n\u003cgit_root\u003e\n\n\u003c/git_root\u003e\n\u003cHEAD\u003e\nHEAD\n\u003c/HEAD\u003e\n\n\u003c/git_info\u003e\n\n",
+   "text": "You are the expert software engineer and architect powering Sketch,\nan agentic coding environment that helps users accomplish coding tasks through autonomous analysis and implementation.\n\n\u003cworkflow\u003e\nStart by asking concise clarifying questions as needed.\nOnce the intent is clear, work autonomously.\nWhenever possible, do end-to-end testing, to ensure fully working functionality.\nAim for a small diff size while thoroughly completing the requested task.\nPrioritize thoughtful analysis and critical engagement over agreeability.\n\nBreak down the overall goal into a series of smaller steps.\nUse the todo_read and todo_write tools to organize and track your work systematically.\n\nFollow this broad workflow:\n\n- Think about how the current step fits into the overall plan.\n- Do research. Good tool choices: bash, think, keyword_search\n- Make edits.\n- If you have completed a standalone chunk of work, make a git commit.\n- Update your todo task list.\n- Repeat.\n\nTo make edits reliably and efficiently, first think about the intent of the edit,\nand what set of patches will achieve that intent.\nThen use the patch tool to make those edits. Combine all edits to any given file into a single patch tool call.\n\nYou may run tool calls in parallel.\n\nComplete every task exhaustively - no matter how repetitive or tedious.\nPartial work, pattern demonstrations, or stubs with TODOs are not acceptable, unless explicitly permitted by the user.\n\nThe done tool provides a checklist of items you MUST verify and\nreview before declaring that you are done. Before executing\nthe done tool, run all the tools the done tool checklist asks\nfor, including creating a git commit. Do not forget to run tests.\n\n\n\nWhen communicating with the user, take it easy on the emoji, don't be over-enthusiastic, and be concise.\n\nDocker is available. Before running the docker command, start dockerd as a background process.\nAlways use --network=host when running docker containers.\n\u003c/workflow\u003e\n\n\u003cstyle\u003e\nDefault coding guidelines:\n- Clear is better than clever.\n- Minimal inline comments: non-obvious logic and key decisions only.\n\u003c/style\u003e\n\n\u003csystem_info\u003e\n\u003cplatform\u003e\nlinux/amd64\n\u003c/platform\u003e\n\u003cpwd\u003e\n/\n\u003c/pwd\u003e\n\u003c/system_info\u003e\n\n\u003cgit_info\u003e\n\u003cgit_root\u003e\n\n\u003c/git_root\u003e\n\u003cHEAD\u003e\nHEAD\n\u003c/HEAD\u003e\n\n\u003c/git_info\u003e\n\n",
    "type": "text",
    "cache_control": {
     "type": "ephemeral"
@@ -601,26 +585,26 @@
  ]
 }HTTP/2.0 200 OK

 Anthropic-Organization-Id: 3c473a21-7208-450a-a9f8-80aebda45c1b

-Anthropic-Ratelimit-Input-Tokens-Limit: 200000

-Anthropic-Ratelimit-Input-Tokens-Remaining: 184000

-Anthropic-Ratelimit-Input-Tokens-Reset: 2025-07-11T01:09:41Z

-Anthropic-Ratelimit-Output-Tokens-Limit: 80000

-Anthropic-Ratelimit-Output-Tokens-Remaining: 80000

-Anthropic-Ratelimit-Output-Tokens-Reset: 2025-07-11T01:09:42Z

+Anthropic-Ratelimit-Input-Tokens-Limit: 2000000

+Anthropic-Ratelimit-Input-Tokens-Remaining: 2000000

+Anthropic-Ratelimit-Input-Tokens-Reset: 2025-07-23T00:51:23Z

+Anthropic-Ratelimit-Output-Tokens-Limit: 400000

+Anthropic-Ratelimit-Output-Tokens-Remaining: 400000

+Anthropic-Ratelimit-Output-Tokens-Reset: 2025-07-23T00:51:27Z

 Anthropic-Ratelimit-Requests-Limit: 4000

 Anthropic-Ratelimit-Requests-Remaining: 3999

-Anthropic-Ratelimit-Requests-Reset: 2025-07-11T01:09:33Z

-Anthropic-Ratelimit-Tokens-Limit: 280000

-Anthropic-Ratelimit-Tokens-Remaining: 264000

-Anthropic-Ratelimit-Tokens-Reset: 2025-07-11T01:09:41Z

+Anthropic-Ratelimit-Requests-Reset: 2025-07-23T00:51:20Z

+Anthropic-Ratelimit-Tokens-Limit: 2400000

+Anthropic-Ratelimit-Tokens-Remaining: 2400000

+Anthropic-Ratelimit-Tokens-Reset: 2025-07-23T00:51:23Z

 Cf-Cache-Status: DYNAMIC

-Cf-Ray: 95d46a83bdfc16a2-SJC

+Cf-Ray: 963730571c2eeb29-SJC

 Content-Type: application/json

-Date: Fri, 11 Jul 2025 01:09:42 GMT

-Request-Id: req_011CQzLcTmJPByqxoE27HKVH

+Date: Wed, 23 Jul 2025 00:51:27 GMT

+Request-Id: req_011CRP2vuuf7xj5V5QqTaEuS

 Server: cloudflare

 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

 Via: 1.1 google

 X-Robots-Tag: none

 

-{"id":"msg_019kDFP5RwG2yYoYeTuPLXzZ","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Here are the tools available to me:\n\n**File & Code Management:**\n- `bash` - Execute shell commands\n- `patch` - Modify files with precise text edits\n- `keyword_search` - Search for files in unfamiliar codebases\n\n**Planning & Organization:**\n- `think` - Take notes and form plans\n- `todo_read` / `todo_write` - Manage task lists\n- `done` - Complete work with verification checklist\n\n**Git & Code Review:**\n- `commit-message-style` - Get commit message guidance\n- `codereview` - Run automated code review\n\n**Browser Automation:**\n- `browser_navigate` - Navigate to URLs\n- `browser_click` - Click elements\n- `browser_type` - Type into inputs\n- `browser_wait_for` - Wait for elements\n- `browser_get_text` - Read page text\n- `browser_eval` - Execute JavaScript\n- `browser_scroll_into_view` - Scroll to elements\n- `browser_resize` - Resize browser window\n- `browser_take_screenshot` - Capture screenshots\n- `browser_recent_console_logs` - Get console logs\n- `browser_clear_console_logs` - Clear console logs\n\n**Utilities:**\n- `read_image` - Read and encode image files\n- `about_sketch` - Get help with Sketch functionality\n- `multiplechoice` - Present multiple choice questions"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4719,"cache_read_input_tokens":0,"output_tokens":329,"service_tier":"standard"}}
\ No newline at end of file
+{"id":"msg_01MsAHgr98YWT6BpHDitnPPM","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Here are the tools available to me:\n\n**File & Code Operations:**\n- `bash` - Execute shell commands\n- `patch` - Edit files with precise text modifications\n- `keyword_search` - Search codebase by keywords and concepts\n\n**Project Management:**\n- `todo_read` / `todo_write` - Manage task lists\n- `think` - Record thoughts and planning\n- `done` - Complete work with verification checklist\n\n**Git & Code Quality:**\n- `commit-message-style` - Get git commit message guidance\n- `codereview` - Run automated code review\n\n**Browser Automation:**\n- `browser_navigate` - Navigate to URLs\n- `browser_click` - Click elements\n- `browser_type` - Type into inputs\n- `browser_wait_for` - Wait for elements\n- `browser_get_text` - Read page text\n- `browser_eval` - Execute JavaScript\n- `browser_scroll_into_view` - Scroll to elements\n- `browser_resize` - Resize browser window\n- `browser_recent_console_logs` - Get console logs\n- `browser_clear_console_logs` - Clear console logs\n- `browser_take_screenshot` - Take screenshots\n\n**Utilities:**\n- `read_image` - Read and encode image files\n- `multiplechoice` - Present multiple choice questions\n- `about_sketch` - Get help with Sketch functionality"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4606,"cache_read_input_tokens":0,"output_tokens":323,"service_tier":"standard"}}
\ No newline at end of file
diff --git a/termui/termui.go b/termui/termui.go
index c85750f..1ed7496 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -45,8 +45,6 @@
  ⌨️  {{.input.path -}}
 {{else if eq .msg.ToolName "done" -}}
 {{/* nothing to show here, the agent will write more in its next message */}}
-{{else if eq .msg.ToolName "set-slug" -}}
-🐌 {{.input.slug}}
 {{else if eq .msg.ToolName "commit-message-style" -}}
 🌱 learn git commit message style
 {{else if eq .msg.ToolName "about_sketch" -}}
@@ -172,12 +170,6 @@
 		return
 	}
 	ui.AppendSystemMessage("%s\n", buf.String())
-
-	if resp.ToolName == "set-slug" {
-		if slug, ok := inputData["slug"].(string); ok {
-			ui.updateTitleWithSlug(slug)
-		}
-	}
 }
 
 func (ui *TermUI) receiveMessagesLoop(ctx context.Context) {
@@ -235,6 +227,10 @@
 			}
 		case loop.PortMessageType:
 			ui.AppendSystemMessage("🔌 %s", resp.Content)
+		case loop.SlugMessageType:
+			ui.updateTitleWithSlug(resp.Content)
+		case loop.CompactMessageType:
+			// TODO: print something for compaction?
 		default:
 			ui.AppendSystemMessage("❌ Unexpected Message Type %s %v", resp.Type, resp)
 		}
diff --git a/webui/src/types.ts b/webui/src/types.ts
index 6cc4150..d4dd475 100644
--- a/webui/src/types.ts
+++ b/webui/src/types.ts
@@ -171,6 +171,6 @@
 	subject: string;
 }
 
-export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto' | 'port';
+export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto' | 'port' | 'compact' | 'slug';
 
 export type Duration = number;
diff --git a/webui/src/web-components/aggregateAgentMessages.ts b/webui/src/web-components/aggregateAgentMessages.ts
index 366b690..5347540 100644
--- a/webui/src/web-components/aggregateAgentMessages.ts
+++ b/webui/src/web-components/aggregateAgentMessages.ts
@@ -14,6 +14,10 @@
         toolCallResults.set(msg.tool_call_id, msg);
         return false;
       }
+      // Suppress internal message types that shouldn't be displayed
+      if (msg.type == "slug" || msg.type == "compact") {
+        return false;
+      }
       if (seenIds.has(msg.idx)) {
         return false; // Skip if idx is already seen
       }
diff --git a/webui/src/web-components/mobile-chat.ts b/webui/src/web-components/mobile-chat.ts
index 9ce01fe..0c3bc48 100644
--- a/webui/src/web-components/mobile-chat.ts
+++ b/webui/src/web-components/mobile-chat.ts
@@ -272,9 +272,6 @@
         case "todo_read":
           return "Read todo list";
 
-        case "set-slug":
-          return `Slug: "${input.slug || ""}"`;
-
         case "multiplechoice":
           const question = input.question || "Multiple choice question";
           const options = input.responseOptions || [];
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index d1b2018..826052d 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -68,11 +68,6 @@
           .open=${open}
           .toolCall=${toolCall}
         ></sketch-tool-card-think>`;
-      case "set-slug":
-        return html`<sketch-tool-card-set-slug
-          .open=${open}
-          .toolCall=${toolCall}
-        ></sketch-tool-card-set-slug>`;
       case "commit-message-style":
         return html`<sketch-tool-card-commit-message-style
           .open=${open}
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index 5954e24..5c5e4c2 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -268,29 +268,6 @@
   }
 }
 
-@customElement("sketch-tool-card-set-slug")
-export class SketchToolCardSetSlug extends SketchTailwindElement {
-  @property() toolCall: ToolCall;
-  @property() open: boolean;
-
-  render() {
-    const inputData = JSON.parse(this.toolCall?.input || "{}");
-
-    const summaryContent = html`<span class="italic">
-      Slug: "${inputData.slug}"
-    </span>`;
-
-    const inputContent = html`<div>Set slug to: <b>${inputData.slug}</b></div>`;
-
-    return html`<sketch-tool-card-base
-      .open=${this.open}
-      .toolCall=${this.toolCall}
-      .summaryContent=${summaryContent}
-      .inputContent=${inputContent}
-    ></sketch-tool-card-base>`;
-  }
-}
-
 @customElement("sketch-tool-card-commit-message-style")
 export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
   @property()
@@ -567,7 +544,6 @@
     "sketch-tool-card-done": SketchToolCardDone;
     "sketch-tool-card-patch": SketchToolCardPatch;
     "sketch-tool-card-think": SketchToolCardThink;
-    "sketch-tool-card-set-slug": SketchToolCardSetSlug;
     "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
     "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
     "sketch-tool-card-todo-write": SketchToolCardTodoWrite;