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
@@ -1,5 +1,5 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import MarkdownTextRenderer from "@/components/MarkdownTextRenderer";
@@ -12,6 +12,67 @@ describe("MarkdownTextRenderer", () => {
expect(link).toHaveClass("text-blue-500", "dark:text-blue-300");
});
it("renders local file links as previewable file references", () => {
const onOpenFilePreview = vi.fn();
render(
<MarkdownTextRenderer onOpenFilePreview={onOpenFilePreview}>
{"Edited [hook.py](/Users/test/project/nanobot/agent/hook.py:12)"}
</MarkdownTextRenderer>,
);
const reference = screen.getByTestId("inline-file-path");
expect(reference).toHaveTextContent("hook.py");
expect(reference).toHaveAttribute(
"aria-label",
"/Users/test/project/nanobot/agent/hook.py",
);
fireEvent.click(reference);
expect(onOpenFilePreview).toHaveBeenCalledWith(
"/Users/test/project/nanobot/agent/hook.py",
);
});
it("does not treat non-file hrefs as previews just because the label looks like a file", () => {
const onOpenFilePreview = vi.fn();
render(
<MarkdownTextRenderer onOpenFilePreview={onOpenFilePreview}>
{"Download [index.html](/api/media/sig/html)"}
</MarkdownTextRenderer>,
);
expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "index.html" })).toHaveAttribute(
"href",
"/api/media/sig/html",
);
});
it("renders glob file links as plain text instead of preview targets", () => {
const onOpenFilePreview = vi.fn();
const { container } = render(
<MarkdownTextRenderer onOpenFilePreview={onOpenFilePreview}>
{"原始对话通常还在 [*.json](*.json)。"}
</MarkdownTextRenderer>,
);
expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument();
expect(screen.queryByRole("link", { name: "*.json" })).not.toBeInTheDocument();
expect(container).toHaveTextContent("*.json");
});
it("keeps glob inline code as code instead of a file preview chip", () => {
render(
<MarkdownTextRenderer>
{"检查 `src/**/*.json`。"}
</MarkdownTextRenderer>,
);
expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument();
expect(screen.getByText("src/**/*.json").tagName).toBe("CODE");
});
it("does not wrap complete fenced code blocks in an extra pre", () => {
const { container } = render(
<MarkdownTextRenderer highlightCode={false}>
@@ -117,6 +178,42 @@ describe("MarkdownTextRenderer", () => {
).toHaveAttribute("href", "https://polymarket.com/event/when-will-gpt-5pt6-be-released");
});
it("falls back through favicon sources before showing a globe for compact link rows", () => {
const { container } = render(
<MarkdownTextRenderer>
{
"Useful links:\n\n- Savills Hong Kong Corporate Relocation — Corporate relocation services\n https://www.savills.com.hk/services/corporate-relocation.aspx"
}
</MarkdownTextRenderer>,
);
const link = screen.getByRole("link", {
name: "Open link: Savills Hong Kong Corporate Relocation — Corporate relocation services",
});
const favicon = () => link.querySelector("img");
expect(favicon()).toHaveAttribute(
"src",
"https://www.savills.com.hk/favicon.ico",
);
fireEvent.error(favicon()!);
expect(favicon()).toHaveAttribute(
"src",
"https://icons.duckduckgo.com/ip3/www.savills.com.hk.ico",
);
fireEvent.error(favicon()!);
expect(favicon()).toHaveAttribute(
"src",
"https://www.google.com/s2/favicons?domain=www.savills.com.hk&sz=64",
);
fireEvent.error(favicon()!);
expect(favicon()).not.toBeInTheDocument();
expect(link.querySelector("svg")).toBeInTheDocument();
expect(container).not.toHaveTextContent("SC");
});
it("renders media attachments without an extra preview/code wrapper", () => {
render(<MarkdownTextRenderer>![Diagram](/api/media/sig/payload)</MarkdownTextRenderer>);