- closebar()} /> diff --git a/src/routes/VerticalLayout/Sidebar.svelte b/src/routes/VerticalLayout/Sidebar.svelte index c86dc71c..17f70cc4 100644 --- a/src/routes/VerticalLayout/Sidebar.svelte +++ b/src/routes/VerticalLayout/Sidebar.svelte @@ -276,8 +276,10 @@ } +
diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index edd023cb..b14d1601 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -50,12 +50,13 @@ import LoadingToComplete from '$lib/common/spinners/LoadingToComplete.svelte'; import AudioSpeaker from '$lib/common/audio-player/AudioSpeaker.svelte'; import CodeScript from '$lib/common/shared/CodeScript.svelte'; + import Markdown from '$lib/common/markdown/Markdown.svelte'; import Label from '$lib/common/shared/Label.svelte'; import { realtimeChat } from '$lib/services/realtime-chat-service'; import { webSpeech } from '$lib/services/web-speech'; import LocalStorageManager from '$lib/helpers/utils/storage-manager'; import { clickoutsideDirective } from '$lib/helpers/directives'; - import { delay, formatNumber } from '$lib/helpers/utils/common'; + import { delay, directToAgentPage, formatNumber, liveRunIdInText } from '$lib/helpers/utils/common'; import { AgentExtensions } from '$lib/helpers/utils/agent'; import { utcToLocal } from '$lib/helpers/datetime'; import { replaceNewLine } from '$lib/helpers/http'; @@ -548,12 +549,39 @@ } } + /** + * Drops every live-view link but the most recent one. + * + * These are pushed into the conversation each time a browser task starts, and they + * accumulate: a session that looks up three things leaves three identical-looking + * "Watch this run" lines, only the last of which leads anywhere. The URLs carry a + * short-lived single-run token, so an earlier one is a dead link dressed as a live + * one — and the reader has no way to tell them apart, since the sentence is the same + * every time. Clicking the wrong one is the whole cost. + * + * Hidden at render, not deleted: the messages stay in `dialogs` and in the server's + * history, so message ids, truncation indices and the content log are untouched, and + * a link comes back into view if a later one is ever truncated away. + * + * @param {import('$conversationTypes').ChatResponseModel[]} dialogs + */ + function hideSupersededLiveLinks(dialogs) { + const isLiveLink = dialogs.map(msg => !!liveRunIdInText(msg?.rich_content?.message?.text || msg?.text)); + const latest = isLiveLink.lastIndexOf(true); + + // Nothing to supersede: zero or one live link. Returning the array as-is keeps the + // common case — every conversation that never touches SimpleClaw — free. + if (latest < 0 || isLiveLink.indexOf(true) === latest) return dialogs; + + return dialogs.filter((_msg, idx) => !isLiveLink[idx] || idx === latest); + } + /** @param {import('$conversationTypes').ChatResponseModel[]} dialogs */ function groupDialogs(dialogs) { if (!dialogs) return []; const format = 'MMM D, YYYY'; // @ts-ignore - return _.groupBy(dialogs, (x) => { + return _.groupBy(hideSupersededLiveLinks(dialogs), (x) => { const createDate = moment.utc(x.created_at).local().format(format); if (createDate == moment.utc().local().format(format)) { return 'Today'; @@ -582,11 +610,12 @@ /** @param {import('$conversationTypes').ChatResponseModel} message */ function onMessageReceivedFromAssistant(message) { + const isSameAsLast = dialogs[dialogs.length - 1]?.message_id === message.message_id + && dialogs[dialogs.length - 1]?.sender?.role === UserRole.Assistant + && !dialogs[dialogs.length - 1]?.is_appended; + if (!message.is_streaming) { - if (dialogs[dialogs.length - 1]?.message_id === message.message_id - && dialogs[dialogs.length - 1]?.sender?.role === UserRole.Assistant - && !dialogs[dialogs.length - 1]?.is_appended - ) { + if (isSameAsLast) { dialogs[dialogs.length - 1] = { ...message, is_chat_message: true @@ -597,6 +626,11 @@ is_chat_message: true }); } + } else if (isSameAsLast) { + // The streamed bubble was created on the first BeforeReceiveLlmStreamMessage of the round, + // so it carries the time the request started, not the time this reply was produced. + // This event is the completed response, so take its timestamp. + dialogs[dialogs.length - 1].created_at = message.created_at; } isStreaming = false; @@ -1343,6 +1377,20 @@ }); } + /** + * Deliberately still `window.open`, unlike the other "open in a new tab" buttons in this app + * (which now go through `openAppRoute`). Two reasons it cannot use the same treatment: + * + * - The button only renders when `isFrame`, i.e. this chat is inside the livechat IFRAME. + * `isDesktop()` cannot answer there: Tauri injects `__TAURI_INTERNALS__` into the main + * frame only, so a nested frame always reads as "browser" and the branch would never fire. + * - The target is the CURRENT path. Navigating the one desktop window to the page it is + * already on is a reload, not a full-screen view — a change that would look like a fix and + * do nothing. Escaping the frame needs a real answer (a Tauri window, or dropping the + * surrounding chrome), not a redirect. + * + * So in the desktop shell this button is inert, and it is gated behind PUBLIC_DEBUG_MODE. + */ function openFullScreen() { window.open(page.url.pathname); } @@ -1800,19 +1848,24 @@ {/if} -
-
+
+
-
+
+ {agent?.name || 'Unkown'} +
@@ -1973,6 +2026,31 @@
{#each dialogGroup as message} + {@const runId = BOT_SENDERS.includes(message.sender?.role) + ? liveRunIdInText(message?.rich_content?.message?.text || message?.text) + : null} + {#if runId} + +
  • +
    + + +
    +
  • + {:else}
  • {#if !BOT_SENDERS.includes(message.sender?.role)} @@ -2018,12 +2096,15 @@ class="cb-bubble cb-bubble-user" class:cb-clickable={!isLite && isLoadPersistLog} class:cb-bubble-user-danger={highlightedMsgId === message.message_id} - class:cb-bubble-bounce={highlightedMsgId === message.message_id} id={`user-msg-${message.message_id}`} >
    {@html replaceNewLine(message.text)}
  • +

    + + {utcToLocal(message.created_at, 'h:mm:ss A')} +

    {#if !disableAction}
    @@ -2089,10 +2170,6 @@
    {/if} -

    - - {utcToLocal(message.created_at, 'h:mm:ss A')} -

    {#if !!message.post_action_disclaimer} {/if} @@ -2112,9 +2189,8 @@ avatar {:else} {@const isShowIcon = (message?.rich_content?.message?.text || message?.text || message?.thought?.thinking_text) || message?.uuid !== lastBotMsg?.uuid} - {@const isLastBotIcon = message?.uuid === lastBotMsg?.uuid} avatar {/if} + {#if !!(message?.rich_content?.message?.text || message?.text) && editingBotMsgUid !== message.uuid} + {@const isLastBotMsg = message?.message_id === lastBotMsg?.message_id && message?.uuid === lastBotMsg?.uuid} + + {#if !isLastBotMsg || (!isStreaming && !isHandlingQueue && !isThinking)} +

    + + {utcToLocal(message.created_at, 'h:mm:ss A')} +

    + {/if} + {/if} {#if message?.message_id === lastBotMsg?.message_id && message?.uuid === lastBotMsg?.uuid} {@const isStreamEnd = (message?.rich_content?.message?.text || message?.text) && !isStreaming && !isHandlingQueue && !isThinking}
    @@ -2241,6 +2328,7 @@ {/if}
    + {/if} {/each} {/each} diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte index 017fbd92..42e7b9ac 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte @@ -2,6 +2,7 @@ import { getContext, onMount } from "svelte"; import { fade } from 'svelte/transition'; import { ElementType } from "$lib/helpers/enums"; + import { openExternal } from '$lib/helpers/utils/desktop'; /** * @type {{ @@ -78,7 +79,7 @@ e.preventDefault(); if (option.type === ElementType.Web && option.url) { - window.open(option.url); + openExternal(option.url); return; } diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte index 904d637a..c4e06470 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte @@ -3,6 +3,7 @@ import { fade } from 'svelte/transition'; import SveltePlayer from "svelte-player"; import { ElementType } from "$lib/helpers/enums"; + import { openExternal } from '$lib/helpers/utils/desktop'; import ChatFileUploader from "../chat-util/chat-file-uploader.svelte"; /** @@ -76,7 +77,7 @@ e.preventDefault(); if (option.type === ElementType.Web && option.url) { - window.open(option.url); + openExternal(option.url); return; } diff --git a/src/routes/page/agent/[agentId]/agent-components/agent-instruction.svelte b/src/routes/page/agent/[agentId]/agent-components/agent-instruction.svelte index ff796f27..f555df79 100644 --- a/src/routes/page/agent/[agentId]/agent-components/agent-instruction.svelte +++ b/src/routes/page/agent/[agentId]/agent-components/agent-instruction.svelte @@ -135,13 +135,13 @@
    {agent.name}
    -
    +