diff --git a/coderd/chatd/chatloop/chatloop.go b/coderd/chatd/chatloop/chatloop.go index a2eb67a960..aa4373775b 100644 --- a/coderd/chatd/chatloop/chatloop.go +++ b/coderd/chatd/chatloop/chatloop.go @@ -428,27 +428,6 @@ func processStepStream( activeReasoningContent := make(map[string]reasoningState) // Track tool names by ID for input delta publishing. toolNames := make(map[string]string) - // Track reasoning text/titles for title extraction. - reasoningTitles := make(map[string]string) - reasoningText := make(map[string]string) - - setReasoningTitleFromText := func(id string, text string) { - if id == "" || strings.TrimSpace(text) == "" { - return - } - if reasoningTitles[id] != "" { - return - } - reasoningText[id] += text - if !strings.ContainsAny(reasoningText[id], "\r\n") { - return - } - title := chatprompt.ReasoningTitleFromFirstLine(reasoningText[id]) - if title == "" { - return - } - reasoningTitles[id] = title - } for part := range stream { switch part.Type { @@ -485,12 +464,9 @@ func processStepStream( active.options = part.ProviderMetadata activeReasoningContent[part.ID] = active } - setReasoningTitleFromText(part.ID, part.Delta) - title := reasoningTitles[part.ID] publishMessagePart(fantasy.MessageRoleAssistant, codersdk.ChatMessagePart{ - Type: codersdk.ChatMessagePartTypeReasoning, - Text: part.Delta, - Title: title, + Type: codersdk.ChatMessagePartTypeReasoning, + Text: part.Delta, }) case fantasy.StreamPartTypeReasoningEnd: @@ -504,21 +480,6 @@ func processStepStream( } result.content = append(result.content, content) delete(activeReasoningContent, part.ID) - - // Derive reasoning title at end of reasoning - // block if we haven't yet. - if reasoningTitles[part.ID] == "" { - reasoningTitles[part.ID] = chatprompt.ReasoningTitleFromFirstLine( - reasoningText[part.ID], - ) - } - title := reasoningTitles[part.ID] - if title != "" { - publishMessagePart(fantasy.MessageRoleAssistant, codersdk.ChatMessagePart{ - Type: codersdk.ChatMessagePartTypeReasoning, - Title: title, - }) - } } case fantasy.StreamPartTypeToolInputStart: activeToolCalls[part.ID] = &fantasy.ToolCallContent{ diff --git a/coderd/chatd/chatprompt/chatprompt.go b/coderd/chatd/chatprompt/chatprompt.go index a7d07100cc..df72f5e365 100644 --- a/coderd/chatd/chatprompt/chatprompt.go +++ b/coderd/chatd/chatprompt/chatprompt.go @@ -7,7 +7,6 @@ import ( "strings" "charm.land/fantasy" - fantasyopenai "charm.land/fantasy/providers/openai" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" "golang.org/x/xerrors" @@ -588,8 +587,7 @@ func MarshalContent(blocks []fantasy.Content, fileIDs map[int]uuid.UUID) (pqtype } // injectFileID adds a file_id field into the data sub-object of a -// serialized content block envelope. This follows the same pattern -// as the reasoning title injection in marshalContentBlock. +// serialized content block envelope. func injectFileID(encoded json.RawMessage, fileID uuid.UUID) (json.RawMessage, error) { var envelope struct { Type string `json:"type"` @@ -673,15 +671,13 @@ func PartFromContent(block fantasy.Content) codersdk.ChatMessagePart { } case fantasy.ReasoningContent: return codersdk.ChatMessagePart{ - Type: codersdk.ChatMessagePartTypeReasoning, - Text: value.Text, - Title: reasoningSummaryTitle(value.ProviderMetadata), + Type: codersdk.ChatMessagePartTypeReasoning, + Text: value.Text, } case *fantasy.ReasoningContent: return codersdk.ChatMessagePart{ - Type: codersdk.ChatMessagePartTypeReasoning, - Text: value.Text, - Title: reasoningSummaryTitle(value.ProviderMetadata), + Type: codersdk.ChatMessagePartTypeReasoning, + Text: value.Text, } case fantasy.ToolCallContent: return codersdk.ChatMessagePart{ @@ -778,43 +774,6 @@ func toolResultContentToPart(content fantasy.ToolResultContent) codersdk.ChatMes return ToolResultToPart(content.ToolCallID, content.ToolName, result, isError) } -// ReasoningTitleFromFirstLine extracts a compact markdown title. -func ReasoningTitleFromFirstLine(text string) string { - text = strings.TrimSpace(text) - if text == "" { - return "" - } - - firstLine := text - if idx := strings.IndexAny(firstLine, "\r\n"); idx >= 0 { - firstLine = firstLine[:idx] - } - firstLine = strings.TrimSpace(firstLine) - if firstLine == "" || !strings.HasPrefix(firstLine, "**") { - return "" - } - - rest := firstLine[2:] - end := strings.Index(rest, "**") - if end < 0 { - return "" - } - - title := strings.TrimSpace(rest[:end]) - if title == "" { - return "" - } - - // Require the first line to be exactly "**title**" (ignoring - // surrounding whitespace) so providers without this format don't - // accidentally emit a title. - if strings.TrimSpace(rest[end+2:]) != "" { - return "" - } - - return compactReasoningSummaryTitle(title) -} - func injectMissingToolResults(prompt []fantasy.Message) []fantasy.Message { result := make([]fantasy.Message, 0, len(prompt)) for i := 0; i < len(prompt); i++ { @@ -1019,147 +978,5 @@ func sanitizeToolCallID(id string) string { } func marshalContentBlock(block fantasy.Content) (json.RawMessage, error) { - encoded, err := json.Marshal(block) - if err != nil { - return nil, err - } - - title, ok := reasoningTitleFromContent(block) - if !ok || title == "" { - return encoded, nil - } - - var envelope struct { - Type string `json:"type"` - Data map[string]any `json:"data"` - } - if err := json.Unmarshal(encoded, &envelope); err != nil { - return nil, err - } - - if !strings.EqualFold(envelope.Type, string(fantasy.ContentTypeReasoning)) { - return encoded, nil - } - if envelope.Data == nil { - envelope.Data = map[string]any{} - } - envelope.Data["title"] = title - - encodedWithTitle, err := json.Marshal(envelope) - if err != nil { - return nil, err - } - return encodedWithTitle, nil -} - -func reasoningTitleFromContent(block fantasy.Content) (string, bool) { - switch value := block.(type) { - case fantasy.ReasoningContent: - return ReasoningTitleFromFirstLine(value.Text), true - case *fantasy.ReasoningContent: - if value == nil { - return "", false - } - return ReasoningTitleFromFirstLine(value.Text), true - default: - return "", false - } -} - -func reasoningSummaryTitle(metadata fantasy.ProviderMetadata) string { - if len(metadata) == 0 { - return "" - } - - reasoningMetadata := fantasyopenai.GetReasoningMetadata( - fantasy.ProviderOptions(metadata), - ) - if reasoningMetadata == nil { - return "" - } - - for _, summary := range reasoningMetadata.Summary { - if title := compactReasoningSummaryTitle(summary); title != "" { - return title - } - } - - return "" -} - -func compactReasoningSummaryTitle(summary string) string { - const maxWords = 8 - const maxRunes = 80 - - summary = strings.TrimSpace(summary) - if summary == "" { - return "" - } - - summary = strings.Trim(summary, "\"'`") - summary = reasoningSummaryHeadline(summary) - words := strings.Fields(summary) - if len(words) == 0 { - return "" - } - - truncated := false - if len(words) > maxWords { - words = words[:maxWords] - truncated = true - } - - title := strings.Join(words, " ") - if truncated { - title += "…" - } - return truncateRunes(title, maxRunes) -} - -func reasoningSummaryHeadline(summary string) string { - summary = strings.TrimSpace(summary) - if summary == "" { - return "" - } - - // OpenAI summary_text may be markdown like: - // "**Title**\n\nLonger explanation ...". - // Keep only the heading segment for UI titles. - if idx := strings.Index(summary, "\n\n"); idx >= 0 { - summary = summary[:idx] - } - - if idx := strings.IndexAny(summary, "\r\n"); idx >= 0 { - summary = summary[:idx] - } - - summary = strings.TrimSpace(summary) - if summary == "" { - return "" - } - - if strings.HasPrefix(summary, "**") { - rest := summary[2:] - if end := strings.Index(rest, "**"); end >= 0 { - bold := strings.TrimSpace(rest[:end]) - if bold != "" { - summary = bold - } - } - } - - return strings.TrimSpace(strings.Trim(summary, "\"'`")) -} - -func truncateRunes(value string, maxLen int) string { - if maxLen <= 0 { - return "" - } - - runes := []rune(value) - if len(runes) <= maxLen { - return value - } - - return string(runes[:maxLen]) + return json.Marshal(block) } diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 4ef02d3e42..0d6fbb1fd1 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1165,10 +1165,7 @@ func chatMessageParts(role string, raw pqtype.NullRawMessage) ([]codersdk.ChatMe continue } if i < len(rawBlocks) { - switch part.Type { - case codersdk.ChatMessagePartTypeReasoning: - part.Title = reasoningStoredTitle(rawBlocks[i]) - case codersdk.ChatMessagePartTypeFile: + if part.Type == codersdk.ChatMessagePartTypeFile { if fid, err := chatprompt.ExtractFileID(rawBlocks[i]); err == nil { part.FileID = uuid.NullUUID{UUID: fid, Valid: true} } @@ -1267,22 +1264,6 @@ func parseToolResults(raw pqtype.NullRawMessage) ([]toolResultRow, error) { return results, nil } -func reasoningStoredTitle(raw json.RawMessage) string { - var envelope struct { - Type string `json:"type"` - Data struct { - Title string `json:"title"` - } `json:"data"` - } - if err := json.Unmarshal(raw, &envelope); err != nil { - return "" - } - if !strings.EqualFold(envelope.Type, string(fantasy.ContentTypeReasoning)) { - return "" - } - return strings.TrimSpace(envelope.Data.Title) -} - func contentBlockToPart(block fantasy.Content) codersdk.ChatMessagePart { switch value := block.(type) { case fantasy.TextContent: diff --git a/coderd/database/db2sdk/db2sdk_test.go b/coderd/database/db2sdk/db2sdk_test.go index a9a9527370..db550400f1 100644 --- a/coderd/database/db2sdk/db2sdk_test.go +++ b/coderd/database/db2sdk/db2sdk_test.go @@ -9,7 +9,6 @@ import ( "time" "charm.land/fantasy" - fantasyopenai "charm.land/fantasy/providers/openai" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" "github.com/stretchr/testify/require" @@ -438,82 +437,6 @@ func TestAIBridgeInterception(t *testing.T) { } } -func TestChatMessage_ReasoningPartWithoutPersistedTitleIsEmpty(t *testing.T) { - t.Parallel() - - assistantContent, err := json.Marshal([]fantasy.Content{ - fantasy.ReasoningContent{ - Text: "Plan migration", - ProviderMetadata: fantasy.ProviderMetadata{ - fantasyopenai.Name: &fantasyopenai.ResponsesReasoningMetadata{ - ItemID: "reasoning-1", - Summary: []string{"Plan migration"}, - }, - }, - }, - }) - require.NoError(t, err) - - message := db2sdk.ChatMessage(database.ChatMessage{ - ID: 1, - ChatID: uuid.New(), - CreatedAt: time.Now(), - Role: string(fantasy.MessageRoleAssistant), - Content: pqtype.NullRawMessage{ - RawMessage: assistantContent, - Valid: true, - }, - }) - - require.Len(t, message.Content, 1) - require.Equal(t, codersdk.ChatMessagePartTypeReasoning, message.Content[0].Type) - require.Equal(t, "Plan migration", message.Content[0].Text) - require.Empty(t, message.Content[0].Title) -} - -func TestChatMessage_ReasoningPartPrefersPersistedTitle(t *testing.T) { - t.Parallel() - - reasoningContent, err := json.Marshal(fantasy.ReasoningContent{ - Text: "Verify schema updates, then apply changes in order.", - ProviderMetadata: fantasy.ProviderMetadata{ - fantasyopenai.Name: &fantasyopenai.ResponsesReasoningMetadata{ - ItemID: "reasoning-1", - Summary: []string{ - "**Metadata-derived title**\n\nLonger explanation.", - }, - }, - }, - }) - require.NoError(t, err) - - var envelope map[string]any - require.NoError(t, json.Unmarshal(reasoningContent, &envelope)) - dataValue, ok := envelope["data"].(map[string]any) - require.True(t, ok) - dataValue["title"] = "Persisted stream title" - - encodedReasoning, err := json.Marshal(envelope) - require.NoError(t, err) - assistantContent, err := json.Marshal([]json.RawMessage{encodedReasoning}) - require.NoError(t, err) - - message := db2sdk.ChatMessage(database.ChatMessage{ - ID: 1, - ChatID: uuid.New(), - CreatedAt: time.Now(), - Role: string(fantasy.MessageRoleAssistant), - Content: pqtype.NullRawMessage{ - RawMessage: assistantContent, - Valid: true, - }, - }) - - require.Len(t, message.Content, 1) - require.Equal(t, codersdk.ChatMessagePartTypeReasoning, message.Content[0].Type) - require.Equal(t, "Persisted stream title", message.Content[0].Title) -} - func TestChatQueuedMessage_ParsesUserContentParts(t *testing.T) { t.Parallel() diff --git a/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden b/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden index 859d773b85..7fa005a139 100644 --- a/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden +++ b/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden @@ -24,15 +24,13 @@ { "workspace_folder": "/workspace1", "name": "dev1", - "id": "eb9b7f18-c277-48af-af7c-2a8e5fb42bab", - "subagent_id": "72d17819-ea3b-450a-a502-175886583ecf" + "id": "eb9b7f18-c277-48af-af7c-2a8e5fb42bab" }, { "workspace_folder": "/workspace2", "config_path": "/workspace2/.devcontainer/devcontainer.json", "name": "dev2", - "id": "964430ff-f0d9-4fcb-b645-6333cf6ba9f2", - "subagent_id": "40a59d56-d3df-488f-b07d-331c0b774bac" + "id": "964430ff-f0d9-4fcb-b645-6333cf6ba9f2" } ], "api_key_scope": "all" diff --git a/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfplan.json b/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfplan.json index bbf8d7b10a..fc765e999d 100644 --- a/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfplan.json +++ b/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfplan.json @@ -139,8 +139,7 @@ }, "after_unknown": { "agent_id": true, - "id": true, - "subagent_id": true + "id": true }, "before_sensitive": false, "after_sensitive": {} @@ -163,8 +162,7 @@ }, "after_unknown": { "agent_id": true, - "id": true, - "subagent_id": true + "id": true }, "before_sensitive": false, "after_sensitive": {} diff --git a/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfstate.json b/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfstate.json index fac2730e9f..a024d46715 100644 --- a/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfstate.json +++ b/provisioner/terraform/testdata/resources/devcontainer/devcontainer.tfstate.json @@ -60,7 +60,6 @@ "agent_id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e", "config_path": null, "id": "eb9b7f18-c277-48af-af7c-2a8e5fb42bab", - "subagent_id": "72d17819-ea3b-450a-a502-175886583ecf", "workspace_folder": "/workspace1" }, "sensitive_values": {}, @@ -79,7 +78,6 @@ "agent_id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e", "config_path": "/workspace2/.devcontainer/devcontainer.json", "id": "964430ff-f0d9-4fcb-b645-6333cf6ba9f2", - "subagent_id": "40a59d56-d3df-488f-b07d-331c0b774bac", "workspace_folder": "/workspace2" }, "sensitive_values": {}, diff --git a/site/src/pages/AgentsPage/AgentDetail/blockUtils.test.ts b/site/src/pages/AgentsPage/AgentDetail/blockUtils.test.ts index d18d381c36..8347bb6316 100644 --- a/site/src/pages/AgentsPage/AgentDetail/blockUtils.test.ts +++ b/site/src/pages/AgentsPage/AgentDetail/blockUtils.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - appendTextBlock, - asNonEmptyString, - mergeThinkingTitles, -} from "./blockUtils"; +import { appendTextBlock, asNonEmptyString } from "./blockUtils"; import type { RenderBlock } from "./types"; // --------------------------------------------------------------------------- @@ -36,61 +32,6 @@ describe("asNonEmptyString", () => { }); }); -// --------------------------------------------------------------------------- -// mergeThinkingTitles -// --------------------------------------------------------------------------- - -describe("mergeThinkingTitles", () => { - it("merges when both titles are undefined", () => { - expect(mergeThinkingTitles(undefined, undefined)).toEqual({ - shouldMerge: true, - title: undefined, - }); - }); - - it("merges and picks nextTitle when current is undefined", () => { - expect(mergeThinkingTitles(undefined, "Thinking")).toEqual({ - shouldMerge: true, - title: "Thinking", - }); - }); - - it("merges and keeps currentTitle when next is undefined", () => { - expect(mergeThinkingTitles("Thinking", undefined)).toEqual({ - shouldMerge: true, - title: "Thinking", - }); - }); - - it("merges when titles are identical", () => { - expect(mergeThinkingTitles("Thinking", "Thinking")).toEqual({ - shouldMerge: true, - title: "Thinking", - }); - }); - - it("merges and uses nextTitle when it extends currentTitle", () => { - expect(mergeThinkingTitles("Think", "Thinking deeply")).toEqual({ - shouldMerge: true, - title: "Thinking deeply", - }); - }); - - it("merges and keeps currentTitle when it extends nextTitle", () => { - expect(mergeThinkingTitles("Thinking deeply", "Think")).toEqual({ - shouldMerge: true, - title: "Thinking deeply", - }); - }); - - it("does not merge when titles are completely different", () => { - expect(mergeThinkingTitles("Analyzing", "Planning")).toEqual({ - shouldMerge: false, - title: "Planning", - }); - }); -}); - // --------------------------------------------------------------------------- // appendTextBlock // --------------------------------------------------------------------------- @@ -135,20 +76,15 @@ describe("appendTextBlock", () => { }); }); - it("does not merge thinking blocks with incompatible titles", () => { + it("merges thinking blocks with different titles using the new title", () => { const blocks: RenderBlock[] = [ { type: "thinking", text: "part1", title: "Analyzing" }, ]; const result = appendTextBlock(blocks, "thinking", "part2", "Planning"); - expect(result).toHaveLength(2); + expect(result).toHaveLength(1); expect(result[0]).toEqual({ type: "thinking", - text: "part1", - title: "Analyzing", - }); - expect(result[1]).toEqual({ - type: "thinking", - text: "part2", + text: "part1part2", title: "Planning", }); }); @@ -193,7 +129,7 @@ describe("appendTextBlock", () => { expect(result).not.toBe(blocks); }); - it("merges thinking block when nextTitle extends currentTitle", () => { + it("merges thinking block and uses new title", () => { const blocks: RenderBlock[] = [ { type: "thinking", text: "a", title: "Think" }, ]; diff --git a/site/src/pages/AgentsPage/AgentDetail/blockUtils.ts b/site/src/pages/AgentsPage/AgentDetail/blockUtils.ts index f397dad984..2ae1e56f02 100644 --- a/site/src/pages/AgentsPage/AgentDetail/blockUtils.ts +++ b/site/src/pages/AgentsPage/AgentDetail/blockUtils.ts @@ -13,35 +13,9 @@ export const asNonEmptyString = (value: unknown): string | undefined => { return next.length > 0 ? next : undefined; }; -export const mergeThinkingTitles = ( - currentTitle: string | undefined, - nextTitle: string | undefined, -): { shouldMerge: boolean; title: string | undefined } => { - if (!currentTitle && !nextTitle) { - return { shouldMerge: true, title: undefined }; - } - if (!currentTitle) { - return { shouldMerge: true, title: nextTitle }; - } - if (!nextTitle) { - return { shouldMerge: true, title: currentTitle }; - } - if (currentTitle === nextTitle) { - return { shouldMerge: true, title: currentTitle }; - } - if (nextTitle.startsWith(currentTitle)) { - return { shouldMerge: true, title: nextTitle }; - } - if (currentTitle.startsWith(nextTitle)) { - return { shouldMerge: true, title: currentTitle }; - } - return { shouldMerge: false, title: nextTitle }; -}; - /** * Append a text or thinking block to a render block list, merging - * with the previous block when the types match (and thinking titles - * are compatible). + * with the previous block when the types match. * * @param joinText Controls how existing and new text are concatenated * when merging into an existing block. Callers that process @@ -61,23 +35,14 @@ export const appendTextBlock = ( const nextBlocks = [...blocks]; const last = nextBlocks[nextBlocks.length - 1]; if (last && last.type === type) { - const shouldMerge = - type === "response" || - (type === "thinking" && - last.type === "thinking" && - mergeThinkingTitles(last.title, title).shouldMerge); - if (shouldMerge) { - const mergedTitle = - type === "thinking" && last.type === "thinking" - ? mergeThinkingTitles(last.title, title).title - : undefined; - nextBlocks[nextBlocks.length - 1] = createBlock( - type, - joinText(last.text, text), - mergedTitle, - ); - return nextBlocks; - } + nextBlocks[nextBlocks.length - 1] = createBlock( + type, + joinText(last.text, text), + type === "thinking" && last.type === "thinking" + ? (title ?? last.title) + : undefined, + ); + return nextBlocks; } nextBlocks.push(createBlock(type, text, title)); return nextBlocks; diff --git a/site/src/pages/AgentsPage/AgentDetail/streamState.test.ts b/site/src/pages/AgentsPage/AgentDetail/streamState.test.ts index 08d449f7fb..cec13e8935 100644 --- a/site/src/pages/AgentsPage/AgentDetail/streamState.test.ts +++ b/site/src/pages/AgentsPage/AgentDetail/streamState.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { applyMessagePartToStreamState, - applyStreamThinkingTitle, buildStreamTools, createEmptyStreamState, } from "./streamState"; @@ -16,33 +15,6 @@ describe("createEmptyStreamState", () => { }); }); -describe("applyStreamThinkingTitle", () => { - it("returns blocks unchanged when title is undefined", () => { - const blocks = [{ type: "response" as const, text: "hello" }]; - expect(applyStreamThinkingTitle(blocks, undefined)).toBe(blocks); - }); - - it("creates a new thinking block when last block is not thinking", () => { - const blocks = [{ type: "response" as const, text: "hello" }]; - const result = applyStreamThinkingTitle(blocks, "Plan"); - expect(result).toHaveLength(2); - expect(result[1]).toEqual({ type: "thinking", text: "", title: "Plan" }); - }); - - it("merges title into existing thinking block", () => { - const blocks = [ - { type: "thinking" as const, text: "some thought", title: "Old" }, - ]; - const result = applyStreamThinkingTitle(blocks, "Old and more"); - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ - type: "thinking", - text: "some thought", - title: "Old and more", - }); - }); -}); - describe("applyMessagePartToStreamState", () => { it("creates new state with response block from text part on null prev", () => { const result = applyMessagePartToStreamState(null, { @@ -109,6 +81,16 @@ describe("applyMessagePartToStreamState", () => { expect(result).toBe(prev); }); + it("returns prev for thinking part with only title and no text", () => { + const prev = createEmptyStreamState(); + const result = applyMessagePartToStreamState(prev, { + type: "thinking", + text: "", + title: "Some Title", + }); + expect(result).toBe(prev); + }); + it("creates tool call entry from tool-call part", () => { const result = applyMessagePartToStreamState(null, { type: "tool-call", diff --git a/site/src/pages/AgentsPage/AgentDetail/streamState.ts b/site/src/pages/AgentsPage/AgentDetail/streamState.ts index 289075fd9e..20985d714a 100644 --- a/site/src/pages/AgentsPage/AgentDetail/streamState.ts +++ b/site/src/pages/AgentsPage/AgentDetail/streamState.ts @@ -1,5 +1,5 @@ import { asString } from "components/ai-elements/runtimeTypeUtils"; -import { appendTextBlock, mergeThinkingTitles } from "./blockUtils"; +import { appendTextBlock } from "./blockUtils"; import { asOptionalTitle, ensureToolBlock, @@ -7,7 +7,7 @@ import { parseToolResultIsError, } from "./messageParsing"; import { mergeStreamPayload } from "./streamingJson"; -import type { MergedTool, RenderBlock, StreamState } from "./types"; +import type { MergedTool, StreamState } from "./types"; let nextFallbackID = 0; @@ -20,32 +20,6 @@ export const createEmptyStreamState = (): StreamState => ({ /** Streaming variant — uses direct concatenation (the default joinText). */ const appendStreamTextBlock = appendTextBlock; -export const applyStreamThinkingTitle = ( - blocks: RenderBlock[], - title?: string, -): RenderBlock[] => { - if (!title) { - return blocks; - } - const nextBlocks = [...blocks]; - const last = nextBlocks[nextBlocks.length - 1]; - if (last && last.type === "thinking") { - const merged = mergeThinkingTitles(last.title, title); - nextBlocks[nextBlocks.length - 1] = { - type: "thinking", - text: last.text, - title: merged.title, - }; - return nextBlocks; - } - nextBlocks.push({ - type: "thinking", - text: "", - title, - }); - return nextBlocks; -}; - export const applyMessagePartToStreamState = ( prev: StreamState | null, part: Record, @@ -67,16 +41,18 @@ export const applyMessagePartToStreamState = ( case "reasoning": case "thinking": { const text = asString(part.text); - const title = asOptionalTitle(part.title); - if (!text && !title) { + if (!text) { return prev; } - const nextBlocks = text - ? appendStreamTextBlock(nextState.blocks, "thinking", text, title) - : applyStreamThinkingTitle(nextState.blocks, title); + const title = asOptionalTitle(part.title); return { ...nextState, - blocks: nextBlocks, + blocks: appendStreamTextBlock( + nextState.blocks, + "thinking", + text, + title, + ), }; } case "tool-call":