feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)

* feat(desktop): add native host scaffold

* feat(webui): track turns and usage in gateway

* feat(webui): polish desktop chat experience

* feat(apps): add ArcGIS and Joplin logos

* feat(desktop): polish shell and shared surfaces

* fix(webui): avoid preview chips for glob references

* test: align CI expectations for token fallback

* feat(webui): preview prompt rail entries

* feat(webui): add prompt navigator drawer

* style(webui): refine prompt navigator placement

* style(webui): align prompt navigator with header actions

* style(webui): simplify prompt navigator header

* refactor(webui): clean thread resource refresh

* feat(desktop): add native reply notifications

* fix(webui): preserve desktop restart and replay state

* fix(desktop): harden gateway proxy startup

* fix(web): fall back when readability is unavailable

* fix(desktop): hide window instead of closing on macos

* fix(webui): unify desktop header actions

* fix(webui): simplify prompt history rows

* fix(desktop): log notification delivery failures

* chore(desktop): clean source package artifacts

* fix(cron): support one-time relative reminders

* fix(webui): reveal scroll button in place

* Revert "fix(cron): support one-time relative reminders"

This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b.

* refactor(webui): extract token usage heatmap

* docs(desktop): clarify contributor guides

---------

Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
Xubin Ren
2026-06-06 19:49:33 +08:00
committed by GitHub
co-authored by chengyongru
parent a1b9577224
commit ab9f49970d
103 changed files with 10483 additions and 1003 deletions
+47 -3
View File
@@ -6,7 +6,7 @@ import {
useState,
type ReactNode,
} from "react";
import { Check, ChevronRight, Copy, ImageIcon, Sparkles, Wrench } from "lucide-react";
import { Check, ChevronRight, Clock3, Copy, ImageIcon, Sparkles, Wrench } from "lucide-react";
import { useTranslation } from "react-i18next";
import { AttachmentTile } from "@/components/AttachmentTile";
@@ -33,6 +33,7 @@ interface MessageBubbleProps {
showAssistantCopyAction?: boolean;
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
onOpenFilePreview?: (path: string) => void;
}
/**
@@ -49,6 +50,7 @@ export function MessageBubble({
showAssistantCopyAction = true,
cliApps = [],
mcpPresets = [],
onOpenFilePreview,
}: MessageBubbleProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
@@ -129,6 +131,10 @@ export function MessageBubble({
const reasoning = message.role === "assistant" ? message.reasoning ?? "" : "";
const reasoningStreaming = !!(message.role === "assistant" && message.reasoningStreaming);
const hasReasoning = reasoning.length > 0 || reasoningStreaming;
const automationSourceLabel = message.source?.kind === "cron"
? (message.source.label?.trim() || t("message.automationSourceFallback"))
: "";
const automationTriggeredLabel = t("message.automationTriggered");
const showAssistantActions = message.role === "assistant" && !message.isStreaming && !empty;
const showCopyButton = showAssistantCopyAction && showAssistantActions;
@@ -142,13 +148,29 @@ export function MessageBubble({
return (
<div className={cn("w-full text-[15px]", baseAnim)} style={{ lineHeight: "var(--cjk-line-height)" }}>
{hasReasoning ? (
<ReasoningBubble text={reasoning} streaming={reasoningStreaming} hasBodyBelow={!empty} />
<ReasoningBubble
text={reasoning}
streaming={reasoningStreaming}
hasBodyBelow={!empty}
onOpenFilePreview={onOpenFilePreview}
/>
) : null}
{empty && message.isStreaming && !hasReasoning ? (
<TypingDots />
) : empty && message.isStreaming ? null : (
<>
<MarkdownText streaming={!!message.isStreaming}>{message.content}</MarkdownText>
{automationSourceLabel ? (
<AutomationSourceBadge
label={automationSourceLabel}
triggerLabel={automationTriggeredLabel}
/>
) : null}
<MarkdownText
streaming={!!message.isStreaming}
onOpenFilePreview={onOpenFilePreview}
>
{message.content}
</MarkdownText>
{media.length > 0 ? <MessageMedia media={media} align="left" /> : null}
{showAssistantFooterRow ? (
<div className="mt-2 flex min-h-8 flex-wrap items-center gap-x-2 gap-y-1 text-muted-foreground">
@@ -187,6 +209,25 @@ export function MessageBubble({
);
}
function AutomationSourceBadge({ label, triggerLabel }: { label: string; triggerLabel: string }) {
return (
<div
className={cn(
"mb-2 inline-flex max-w-full items-center gap-1.5 rounded-full px-2 py-1",
"border border-sky-500/15 bg-sky-500/[0.06]",
"text-[11px] font-medium leading-none text-sky-700",
"dark:border-sky-300/15 dark:bg-sky-300/[0.08] dark:text-sky-200/80",
)}
title={triggerLabel}
>
<Clock3 className="h-3 w-3 shrink-0" aria-hidden />
<span className="min-w-0 truncate">{label}</span>
<span className="text-current/45" aria-hidden>·</span>
<span className="shrink-0">{triggerLabel}</span>
</div>
);
}
function mergeMcpMentionPresets(
presets: McpPresetInfo[],
attachments: UIMcpPresetAttachment[] | undefined,
@@ -488,6 +529,7 @@ interface ReasoningBubbleProps {
hasBodyBelow: boolean;
/** When true, skip the slide-in wrapper (used inside ``AgentActivityCluster``). */
embeddedInCluster?: boolean;
onOpenFilePreview?: (path: string) => void;
}
/**
@@ -509,6 +551,7 @@ export function ReasoningBubble({
streaming,
hasBodyBelow,
embeddedInCluster = false,
onOpenFilePreview,
}: ReasoningBubbleProps) {
const { t } = useTranslation();
const [userToggled, setUserToggled] = useState(false);
@@ -567,6 +610,7 @@ export function ReasoningBubble({
>
<MarkdownText
streaming={streaming}
onOpenFilePreview={onOpenFilePreview}
className={cn(
"text-[12.5px] italic text-muted-foreground/88",
"prose-p:my-1.5 prose-li:my-0.5",