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
+64 -4
View File
@@ -1,3 +1,5 @@
import type { KeyboardEvent, MouseEvent } from "react";
import {
Tooltip,
TooltipContent,
@@ -6,10 +8,11 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
type FileReferenceKind =
export type FileReferenceKind =
| "default"
| "css"
| "html"
| "javascript"
| "json"
| "markdown"
| "notebook"
@@ -24,6 +27,8 @@ interface FileReferenceChipProps {
active?: boolean;
className?: string;
textClassName?: string;
previewPath?: string;
onOpen?: (path: string) => void;
testId?: string;
}
@@ -34,12 +39,26 @@ export function FileReferenceChip({
active = false,
className,
textClassName,
previewPath,
onOpen,
testId = "inline-file-path",
}: FileReferenceChipProps) {
const { directory, name } = splitFilePath(path);
const kind = fileKindForPath(path);
const displayText = display === "path" ? path.replace(/\\/g, "/") : name;
const fullPath = tooltipPath || path;
const targetPath = previewPath || tooltipPath || path;
const interactive = Boolean(onOpen);
const openPreview = (event: MouseEvent | KeyboardEvent) => {
if (!onOpen) return;
event.preventDefault();
event.stopPropagation();
onOpen(targetPath);
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Enter" && event.key !== " ") return;
openPreview(event);
};
return (
<TooltipProvider delayDuration={500} skipDelayDuration={100}>
<Tooltip>
@@ -50,10 +69,18 @@ export function FileReferenceChip({
<span
data-testid={testId}
aria-label={fullPath}
role={interactive ? "button" : undefined}
tabIndex={interactive ? 0 : undefined}
onClick={interactive ? openPreview : undefined}
onKeyDown={interactive ? onKeyDown : undefined}
className={cn(
"inline-flex max-w-full items-baseline gap-[0.28em] font-medium leading-[inherit]",
"text-sky-600 transition-colors hover:text-sky-700",
"dark:text-sky-300 dark:hover:text-sky-200",
interactive && [
"cursor-pointer rounded-[5px]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/45",
],
)}
>
<FileReferenceIcon kind={kind} />
@@ -100,6 +127,7 @@ export function isLikelyFilePath(value: string): boolean {
const raw = value.trim();
if (!raw || raw.includes("\n")) return false;
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return false;
if (isFilePatternReference(raw)) return false;
if (!/[\\/]/.test(raw) && !/^(dockerfile|makefile|readme|package-lock\.json)$/i.test(raw)) {
return false;
}
@@ -110,7 +138,11 @@ export function isLikelyFilePath(value: string): boolean {
return /\.[a-z0-9][a-z0-9_-]{0,12}$/i.test(name);
}
function splitFilePath(path: string): { directory: string; name: string } {
export function isFilePatternReference(value: string): boolean {
return /[*?[\]{}]/.test(value.trim());
}
export function splitFilePath(path: string): { directory: string; name: string } {
const normalized = path.replace(/\\/g, "/");
const slash = normalized.lastIndexOf("/");
if (slash < 0) return { directory: "", name: path };
@@ -120,7 +152,7 @@ function splitFilePath(path: string): { directory: string; name: string } {
};
}
function fileKindForPath(path: string): FileReferenceKind {
export function fileKindForPath(path: string): FileReferenceKind {
const normalized = path.toLowerCase();
const name = normalized.split(/[\\/]/).pop() ?? normalized;
const ext = name.includes(".") ? name.split(".").pop() ?? "" : "";
@@ -134,7 +166,13 @@ function fileKindForPath(path: string): FileReferenceKind {
case "jsx":
case "tsx":
return "react";
case "js":
case "mjs":
case "cjs":
return "javascript";
case "ts":
case "mts":
case "cts":
return "typescript";
case "html":
case "htm":
@@ -156,7 +194,27 @@ function fileKindForPath(path: string): FileReferenceKind {
}
}
function FileReferenceIcon({ kind }: { kind: FileReferenceKind }) {
export function FileReferenceIcon({ kind }: { kind: FileReferenceKind }) {
if (kind === "python") {
return (
<svg
aria-hidden
className="h-[1em] w-[1em] shrink-0 translate-y-[0.12em]"
viewBox="0 0 24 24"
>
<path
d="M11.9 2.3c-3 0-4.5.8-4.5 2.3v2.1h4.8v.8H5.5C4 7.5 3 8.8 3 10.8v2.1c0 1.8 1.1 3 2.7 3h1.6v-2.3c0-1.7 1.4-3.1 3.1-3.1h4.2c1.3 0 2.3-1 2.3-2.3V4.6c0-1.4-1.5-2.3-4.6-2.3h-.4Z"
fill="#3776AB"
/>
<path
d="M12.1 21.7c3 0 4.5-.8 4.5-2.3v-2.1h-4.8v-.8h6.7c1.5 0 2.5-1.3 2.5-3.3v-2.1c0-1.8-1.1-3-2.7-3h-1.6v2.3c0 1.7-1.4 3.1-3.1 3.1H9.4c-1.3 0-2.3 1-2.3 2.3v3.6c0 1.4 1.5 2.3 4.6 2.3h.4Z"
fill="#FFD43B"
/>
<circle cx="9" cy="5.1" r="0.8" fill="#fff" />
<circle cx="15" cy="18.9" r="0.8" fill="#5C3B00" opacity="0.85" />
</svg>
);
}
if (kind === "react") {
return (
<svg
@@ -234,6 +292,8 @@ function fileKindLabel(kind: FileReferenceKind): string {
return "#";
case "html":
return "H";
case "javascript":
return "JS";
case "json":
return "{}";
case "markdown":