diff --git a/apps/discord-bot/src/features/ResponseBridge.ts b/apps/discord-bot/src/features/ResponseBridge.ts index a7906ea6db0e..82a421a4dbf8 100644 --- a/apps/discord-bot/src/features/ResponseBridge.ts +++ b/apps/discord-bot/src/features/ResponseBridge.ts @@ -29,7 +29,6 @@ import { buildStreamHistoryMarkdownText, DISCORD_MAX_FILES_PER_MESSAGE, imageAttachmentsOf, - shouldAttachT3DeepLink, STREAM_HISTORY_MARKDOWN_NAME, streamHistoryHasAdditionalContent, unpostedAttachments, @@ -67,7 +66,6 @@ import { stripMarkdownImages, type MarkdownImageRef, } from "../presentation/markdownImages.ts"; -import { hasMarkdownTables } from "../presentation/asciiTables.ts"; import { chunkDiscordContent, formatInProgressChunk, @@ -3077,7 +3075,7 @@ export const runBridge = ( * * On turn complete: stream messages are deleted, archived as stream-history.md, * and the final answer is posted as Discord markdown (links live; no ASCII tables), - * with a T3 deep link when the answer is long or has GFM tables. + * with a T3 deep link always on the stats footer. */ const postOrEditAssistantUnlocked = (args: { readonly turnId: string | null; @@ -3487,7 +3485,7 @@ export const runBridge = ( /** * Final delivery for a completed assistant turn: * 1. Post Discord markdown content (chunked if needed; links stay clickable) - * 2. Append · [T3](…#message-…) when multi-chunk or tables (full render in Omegent) + * 2. Always append · [T3](…#message-…) on the stats footer * 3. Attach stream-history.md + chat/local images as files * 4. Delete the in-progress stream messages so only the final answer remains visible */ @@ -3698,8 +3696,8 @@ export const runBridge = ( // Final channel text: strip image embeds but keep readable local file references. // Never leave Working.. or the stream placeholder. - // Keep Discord markdown as-is (links stay clickable). Do not ASCII-ify tables — - // long / table-heavy answers get a T3 deep link for full rendering in Omegent. + // Keep Discord markdown as-is (links stay clickable). Do not ASCII-ify tables. + // Stats footer always gets · [T3](deep link) when the web UI base is configured. const finalText = rewriteMarkdownLocalFileLinksForDiscord({ text: stripWorkingIndicator(stripMarkdownImages(text)), githubUrlsBySrc, @@ -3735,7 +3733,8 @@ export const runBridge = ( ? ["_(done)_"] : []; - // Small italic turn stats on the final answer (model / effort / duration / tokens). + // Small italic turn stats on the final answer (model / effort / duration / tokens), + // then always append · [T3](deep link) on that footer section when the URL is known. const statsThread = yield* Ref.get(latestThreadRef); const statsLine = formatTurnResponseStatsLine({ modelSelection: statsThread?.modelSelection ?? null, @@ -3745,22 +3744,13 @@ export const runBridge = ( }); let finalChunks = appendStatsToMessageChunks(baseFinalChunks, statsLine, DISCORD_LIMIT); - // Long multi-message finals and any answer with GFM tables → · [T3](deep link). - if ( - shouldAttachT3DeepLink({ - text: renderedFinalText, - hasMarkdownTables: hasMarkdownTables(renderedFinalText), - messageChunkCount: finalChunks.length, - }) - ) { - const botConfig = yield* DiscordBotConfig; - const t3Url = buildOmegentThreadMessageUrl({ - webUiBaseUrl: botConfig.webUiBaseUrl, - threadId: input.t3ThreadId, - messageId: t3MessageId, - }); - finalChunks = appendT3DeepLinkToChunks(finalChunks, t3Url, DISCORD_LIMIT); - } + const botConfig = yield* DiscordBotConfig; + const t3Url = buildOmegentThreadMessageUrl({ + webUiBaseUrl: botConfig.webUiBaseUrl, + threadId: input.t3ThreadId, + messageId: t3MessageId, + }); + finalChunks = appendT3DeepLinkToChunks(finalChunks, t3Url, DISCORD_LIMIT); if (finalChunks.length === 0 && files.length === 0) { // Nothing useful to post — just clear any leftover Working.. stream messages. diff --git a/apps/discord-bot/src/presentation/attachments.test.ts b/apps/discord-bot/src/presentation/attachments.test.ts index 9c34c12d551d..58eb4933064b 100644 --- a/apps/discord-bot/src/presentation/attachments.test.ts +++ b/apps/discord-bot/src/presentation/attachments.test.ts @@ -5,7 +5,6 @@ import { attachmentKey, buildStreamHistoryMarkdownText, imageAttachmentsOf, - shouldAttachT3DeepLink, STREAM_HISTORY_MARKDOWN_NAME, streamHistoryHasAdditionalContent, unpostedAttachments, @@ -95,37 +94,18 @@ describe("buildStreamHistoryMarkdownText", () => { }); describe("T3 deep link caption helpers", () => { - it("appends a short same-line T3 link", () => { + it("appends a short same-line T3 link on the stats footer", () => { expect( - withT3DeepLink("Summary", "https://t3vm.tail86038f.ts.net/?thread=tid-1#message-msg-1"), - ).toBe("Summary · [T3](https://t3vm.tail86038f.ts.net/?thread=tid-1#message-msg-1)"); + withT3DeepLink( + "_`grok-4.5` · effort high · 50s_", + "https://t3vm.tail86038f.ts.net/?thread=tid-1#message-msg-1", + ), + ).toBe( + "_`grok-4.5` · effort high · 50s_ · [T3](https://t3vm.tail86038f.ts.net/?thread=tid-1#message-msg-1)", + ); expect(withT3DeepLink("Summary", null)).toBe("Summary"); }); - it("links when the answer has tables or needs multiple chunks", () => { - expect( - shouldAttachT3DeepLink({ - text: "short", - hasMarkdownTables: false, - messageChunkCount: 1, - }), - ).toBe(false); - expect( - shouldAttachT3DeepLink({ - text: "long", - hasMarkdownTables: false, - messageChunkCount: 2, - }), - ).toBe(true); - expect( - shouldAttachT3DeepLink({ - text: "| A | B |\n|---|---|\n| 1 | 2 |", - hasMarkdownTables: true, - messageChunkCount: 1, - }), - ).toBe(true); - }); - it("appends the link onto the last chunk without overflowing", () => { const url = "https://t3vm.example/?thread=t#message-m"; expect(appendT3DeepLinkToChunks(["hello"], url, 2000)).toEqual([`hello · [T3](${url})`]); diff --git a/apps/discord-bot/src/presentation/attachments.ts b/apps/discord-bot/src/presentation/attachments.ts index ac011686558f..a2c0a6503841 100644 --- a/apps/discord-bot/src/presentation/attachments.ts +++ b/apps/discord-bot/src/presentation/attachments.ts @@ -68,6 +68,7 @@ export function buildStreamHistoryMarkdownFile(streamText: string): File | null /** * Append a compact T3 deep link on a Discord caption/chunk. * Same-line ` · [T3](url)` — clickable Discord markdown, short label. + * Used on the stats footer of every final answer. */ export function withT3DeepLink(caption: string, t3Url: string | null | undefined): string { const url = t3Url?.trim() ?? ""; @@ -79,22 +80,8 @@ export function withT3DeepLink(caption: string, t3Url: string | null | undefined } /** - * When the final answer would need multi-message chunking or contains GFM tables, - * surface a T3 deep link so the full rendered answer is one click away. - * Short single-message prose without tables stays link-free. - */ -export function shouldAttachT3DeepLink(input: { - readonly text: string; - readonly hasMarkdownTables: boolean; - readonly messageChunkCount: number; -}): boolean { - if (input.text.trim() === "") return false; - if (input.hasMarkdownTables) return true; - return input.messageChunkCount > 1; -} - -/** - * Append a T3 deep link onto the last message chunk, respecting the Discord limit. + * Append a T3 deep link onto the last message chunk (the stats footer), + * respecting the Discord limit. Always used on finals when a URL is available. * If the link would overflow the last chunk, emit it as its own trailing chunk. */ export function appendT3DeepLinkToChunks(