feat(webui): polish native host experience
This commit is contained in:
@@ -67,7 +67,8 @@ export function Sidebar(props: SidebarProps) {
|
||||
ref={props.containActionMenus ? setMenuPortalContainer : undefined}
|
||||
aria-label={t("sidebar.navigation")}
|
||||
className={cn(
|
||||
"flex h-full w-full min-w-0 flex-col bg-sidebar text-sidebar-foreground",
|
||||
"flex h-full w-full min-w-0 flex-col text-sidebar-foreground",
|
||||
props.hostChromeInset ? "bg-transparent" : "bg-sidebar",
|
||||
!props.hostChromeInset && "border-r border-sidebar-border/60",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4277,7 +4277,7 @@ function ProviderPicker({
|
||||
const disabled = providers.length === 0;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -5098,11 +5098,12 @@ function ModelPresetPicker({
|
||||
const selectedPreset = presets.find((preset) => preset.name === value) ?? presets[0] ?? null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild disabled={!presets.length}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
aria-label={tx("settings.rows.currentModel", "Current configuration")}
|
||||
disabled={!presets.length}
|
||||
className={cn(
|
||||
"h-12 w-[min(430px,72vw)] justify-between rounded-full border-input bg-background px-3.5 text-[13px] font-normal shadow-none",
|
||||
@@ -5155,7 +5156,9 @@ function ModelPresetPicker({
|
||||
})}
|
||||
<div className="mt-1 border-t border-border/55 pt-1">
|
||||
<DropdownMenuItem
|
||||
onSelect={onCreateConfiguration}
|
||||
onSelect={() => {
|
||||
window.setTimeout(onCreateConfiguration, 0);
|
||||
}}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-[12px] px-2.5 py-2 text-[13px] font-medium",
|
||||
"text-foreground focus:bg-muted/85 focus:text-foreground",
|
||||
|
||||
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user