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
+108 -30
View File
@@ -1,16 +1,29 @@
import { Children, isValidElement, useMemo, type ReactNode } from "react";
import {
Children,
isValidElement,
useCallback,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import type { Components, Options as ReactMarkdownOptions } from "react-markdown";
import ReactMarkdown from "react-markdown";
import rehypeKatex from "rehype-katex";
import { Check } from "lucide-react";
import { Check, Globe2 } from "lucide-react";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { AttachmentTile } from "@/components/AttachmentTile";
import { CodeBlock } from "@/components/CodeBlock";
import { FileReferenceChip, isLikelyFilePath } from "@/components/FileReferenceChip";
import {
FileReferenceChip,
isFilePatternReference,
isLikelyFilePath,
} from "@/components/FileReferenceChip";
import { inferMediaKind } from "@/lib/media";
import { faviconUrls } from "@/lib/provider-brand";
import { cn } from "@/lib/utils";
import "katex/dist/katex.min.css";
@@ -19,6 +32,7 @@ interface MarkdownTextRendererProps {
children: string;
className?: string;
highlightCode?: boolean;
onOpenFilePreview?: (path: string) => void;
}
type MarkdownAstNode = {
@@ -32,10 +46,9 @@ type MarkdownAstNode = {
type InlineLinkPreview = {
href: string;
origin: string;
host: string;
prefix?: string;
title: string;
initials: string;
};
const SAFE_INLINE_HTML_TAGS = new Set(["mark", "sub", "sup"]);
@@ -187,6 +200,45 @@ function nodeText(value: ReactNode): string {
.join("");
}
function cleanFileReferenceTarget(value: string): string {
let target = value.trim();
if (!target) return "";
try {
if (/^file:\/\//i.test(target)) {
target = decodeURIComponent(new URL(target).pathname);
} else {
target = decodeURIComponent(target);
}
} catch {
// Keep the raw value when URL/path decoding is not possible.
}
target = target.split("?", 1)[0]?.split("#", 1)[0]?.trim() ?? "";
if (!/^[A-Za-z]:[\\/]/.test(target)) {
target = target.replace(/:\d+(?::\d+)?$/, "");
}
return target;
}
function isPreviewableFileTarget(value: string): boolean {
if (isFilePatternReference(value)) return false;
if (isLikelyFilePath(value)) return true;
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false;
if (/[\\/]/.test(value)) return false;
return /^[^?#]+\.[a-z0-9][a-z0-9_-]{0,12}$/i.test(value);
}
function isNonNavigableFilePatternLink(href: string | undefined): boolean {
if (!href || /^https?:\/\//i.test(href) || href.startsWith("#")) return false;
const target = cleanFileReferenceTarget(href);
return Boolean(target && isFilePatternReference(target));
}
function fileReferenceFromLink(href: string | undefined): string | null {
if (!href || /^https?:\/\//i.test(href) || href.startsWith("#")) return null;
const target = cleanFileReferenceTarget(href);
return isPreviewableFileTarget(target) ? target : null;
}
function linkPreviewParts(value: ReactNode): { text: string; href?: string } {
let text = "";
let href: string | undefined;
@@ -216,16 +268,6 @@ function cleanLinkPreviewText(value: string): string {
.trim();
}
function linkPreviewInitials(value: string): string {
const clean = value
.replace(/^https?:\/\//i, "")
.replace(/^www\./i, "")
.replace(/\.[a-z]{2,}$/i, "");
const parts = clean.split(/[\s.-]+/).filter(Boolean);
return (parts.length > 1 ? parts.slice(0, 2).map((part) => part[0]).join("") : clean.slice(0, 2))
.toUpperCase();
}
function inlineLinkPreviewFromChildren(children: ReactNode): InlineLinkPreview | null {
const { text: rawText, href } = linkPreviewParts(children);
if (!href) return null;
@@ -253,17 +295,18 @@ function inlineLinkPreviewFromChildren(children: ReactNode): InlineLinkPreview |
return {
href,
origin: url.origin,
host: url.hostname,
prefix,
title,
initials: linkPreviewInitials(prefix || url.hostname),
};
}
function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
const { favicon, onFaviconError } = useFaviconFallback(link.host);
const label = link.prefix
? `${link.prefix}${link.title}`
: link.title;
return (
<a
href={link.href}
@@ -278,20 +321,21 @@ function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
<span
className={cn(
"relative grid h-4 w-4 shrink-0 place-items-center overflow-hidden rounded-[4px]",
"border border-border/65 bg-background text-[0.5rem] font-semibold text-muted-foreground",
"border border-border/65 bg-background text-muted-foreground",
)}
aria-hidden
>
{link.initials}
<img
src={`${link.origin}/favicon.ico`}
alt=""
className="absolute h-3 w-3 rounded-[2px] object-contain"
loading="lazy"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
{favicon ? (
<img
src={favicon}
alt=""
className="h-3 w-3 rounded-[2px] object-contain"
loading="lazy"
onError={onFaviconError}
/>
) : (
<Globe2 className="h-3 w-3" />
)}
</span>
<span className="min-w-0 truncate leading-normal">
{label}
@@ -300,6 +344,24 @@ function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
);
}
function useFaviconFallback(host: string) {
const faviconCandidates = useMemo(() => faviconUrls(host), [host]);
const [faviconIndex, setFaviconIndex] = useState(0);
useEffect(() => {
setFaviconIndex(0);
}, [host]);
const onFaviconError = useCallback(() => {
setFaviconIndex((index) => Math.min(index + 1, faviconCandidates.length));
}, [faviconCandidates.length]);
return {
favicon: faviconCandidates[faviconIndex] ?? null,
onFaviconError,
};
}
function isRenderedCodeBlock(value: ReactNode): boolean {
if (!isValidElement(value)) return false;
const props = value.props as { code?: unknown };
@@ -326,6 +388,7 @@ export default function MarkdownTextRenderer({
children,
className,
highlightCode = true,
onOpenFilePreview,
}: MarkdownTextRendererProps) {
const components = useMemo<Components>(
() => ({
@@ -344,7 +407,7 @@ export default function MarkdownTextRenderer({
}
const raw = String(kids).replace(/\n$/, "");
if (isLikelyFilePath(raw)) {
return <FileReferenceChip path={raw} />;
return <FileReferenceChip path={raw} onOpen={onOpenFilePreview} />;
}
/** Plain fenced ``` blocks (no language) & wide one-liners: block monospace, not inline pill. */
const widePlainBlock = raw.includes("\n") || raw.length > 120;
@@ -405,6 +468,21 @@ export default function MarkdownTextRenderer({
);
},
a({ href, children: markdownChildren, ...props }) {
const filePath = fileReferenceFromLink(href);
if (filePath) {
const label = nodeText(markdownChildren).trim();
return (
<FileReferenceChip
path={label || filePath}
tooltipPath={filePath}
previewPath={filePath}
onOpen={onOpenFilePreview}
/>
);
}
if (isNonNavigableFilePatternLink(href)) {
return <>{markdownChildren}</>;
}
return (
<a
href={href}
@@ -495,7 +573,7 @@ export default function MarkdownTextRenderer({
);
},
}),
[highlightCode],
[highlightCode, onOpenFilePreview],
);
return (