feat(webui): polish native host experience

This commit is contained in:
Xubin Ren
2026-06-01 00:00:37 +08:00
parent 15c6abc991
commit 31722120b7
14 changed files with 316 additions and 60 deletions
+19 -13
View File
@@ -10,6 +10,7 @@ interface ThreadHeaderProps {
theme: "light" | "dark";
onToggleTheme: () => void;
hideSidebarToggleForHostChrome?: boolean;
hideThemeButton?: boolean;
minimal?: boolean;
}
@@ -19,6 +20,7 @@ export function ThreadHeader({
theme,
onToggleTheme,
hideSidebarToggleForHostChrome = false,
hideThemeButton = false,
minimal = false,
}: ThreadHeaderProps) {
const { t } = useTranslation();
@@ -37,12 +39,14 @@ export function ThreadHeader({
>
<Menu className="h-3.5 w-3.5" />
</Button>
<ThemeButton
theme={theme}
onToggleTheme={onToggleTheme}
label={t("thread.header.toggleTheme")}
className="ml-auto"
/>
{!hideThemeButton ? (
<ThemeButton
theme={theme}
onToggleTheme={onToggleTheme}
label={t("thread.header.toggleTheme")}
className="ml-auto"
/>
) : null}
</div>
);
}
@@ -67,12 +71,14 @@ export function ThreadHeader({
</div>
</div>
<ThemeButton
theme={theme}
onToggleTheme={onToggleTheme}
label={t("thread.header.toggleTheme")}
className="ml-auto shrink-0"
/>
{!hideThemeButton ? (
<ThemeButton
theme={theme}
onToggleTheme={onToggleTheme}
label={t("thread.header.toggleTheme")}
className="ml-auto shrink-0"
/>
) : null}
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-full h-4" />
</div>
@@ -97,7 +103,7 @@ function ThemeButton({
aria-label={label}
onClick={onToggleTheme}
className={cn(
"h-8 w-8 rounded-full text-muted-foreground/85 hover:bg-accent/40 hover:text-foreground",
"host-no-drag h-8 w-8 rounded-full text-muted-foreground/85 hover:bg-accent/40 hover:text-foreground",
className,
)}
>
@@ -62,6 +62,7 @@ interface ThreadShellProps {
theme?: "light" | "dark";
onToggleTheme?: () => void;
hideSidebarToggleForHostChrome?: boolean;
hideThemeButton?: boolean;
hideHeader?: boolean;
workspaceScope?: WorkspaceScopePayload | null;
workspaceDefaultScope?: WorkspaceScopePayload | null;
@@ -142,6 +143,7 @@ export function ThreadShell({
theme = "light",
onToggleTheme = () => {},
hideSidebarToggleForHostChrome = false,
hideThemeButton = false,
hideHeader = false,
workspaceScope = null,
workspaceDefaultScope = null,
@@ -567,6 +569,7 @@ export function ThreadShell({
theme={theme}
onToggleTheme={onToggleTheme}
hideSidebarToggleForHostChrome={hideSidebarToggleForHostChrome}
hideThemeButton={hideThemeButton}
minimal={!session && !loading}
/>
) : null}
@@ -237,7 +237,7 @@ export function ThreadViewport({
<div
ref={scrollRef}
className={cn(
"absolute inset-0 overflow-y-auto scroll-auto scrollbar-thin",
"thread-viewport-scrollbar absolute inset-0 overflow-y-auto scroll-auto scrollbar-thin",
"[&::-webkit-scrollbar]:w-1.5",
"[&::-webkit-scrollbar-thumb]:rounded-full",
"[&::-webkit-scrollbar-thumb]:bg-muted-foreground/30",
@@ -38,6 +38,7 @@ function FileEditRow({ edit }: { edit: FileEditSummary }) {
const editing = edit.status === "editing";
const failed = edit.status === "error";
const hasCountedDiff = !failed && !edit.binary && hasVisibleDiffStats(edit);
const rawFailureDetail = failed ? cleanFileEditError(edit.error) : "";
const failureDetail = failed
? formatFileEditError(edit.error)
|| t("message.fileEditFailedFallback", { defaultValue: "File change was not applied." })
@@ -67,8 +68,8 @@ function FileEditRow({ edit }: { edit: FileEditSummary }) {
active={editing}
tone={failed ? "error" : editing ? "active" : "success"}
className="text-xs"
contentClassName="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3"
title={failureDetail || edit.absolute_path || edit.path}
contentClassName={failed ? "min-w-0" : "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3"}
title={rawFailureDetail || edit.absolute_path || edit.path}
label={edit.pending && !edit.path
? t("message.fileEditPreparing", { defaultValue: "Preparing file edit…" })
: (
@@ -82,13 +83,15 @@ function FileEditRow({ edit }: { edit: FileEditSummary }) {
testId="activity-file-reference"
/>
)}
detail={failed ? (
<span className="min-w-0 truncate text-[11px] leading-4 text-destructive/75">
detail={null}
aside={hasCountedDiff ? <DiffPair added={edit.added} deleted={edit.deleted} /> : null}
>
{failed ? (
<span className="block max-w-[42rem] truncate text-[11px] leading-4 text-destructive/75">
{failureDetail}
</span>
) : null}
aside={hasCountedDiff ? <DiffPair added={edit.added} deleted={edit.deleted} /> : null}
/>
</ActivityStep>
);
}
@@ -96,14 +99,23 @@ export function hasVisibleDiffStats(edit: Pick<FileEditSummary, "added" | "delet
return edit.added > 0 || edit.deleted > 0;
}
function formatFileEditError(error?: string): string {
function cleanFileEditError(error?: string): string {
const firstLine = (error || "").replace(/\s+/g, " ").trim();
if (!firstLine) return "";
const cleaned = firstLine
return firstLine
.replace(/^Error applying patch:\s*/i, "")
.replace(/^Error writing file:\s*/i, "")
.replace(/^Error editing file:\s*/i, "")
.replace(/^Error:\s*/i, "");
}
function formatFileEditError(error?: string): string {
const cleaned = cleanFileEditError(error);
if (!cleaned) return "";
if (/\bpermission denied\b/i.test(cleaned) || /\boperation not permitted\b/i.test(cleaned)) {
return "No permission to change this location.";
}
return cleaned
.replace(/^old_text not found in (.+)$/i, "Target text was not found in $1.")