diff --git a/benchmarks/terminal_bench/prepare_leaderboard_submission.py b/benchmarks/terminal_bench/prepare_leaderboard_submission.py index 4b94e293b8b..e379b25e5a9 100755 --- a/benchmarks/terminal_bench/prepare_leaderboard_submission.py +++ b/benchmarks/terminal_bench/prepare_leaderboard_submission.py @@ -216,6 +216,11 @@ def get_model_from_config(config_path: Path) -> str | None: return None +def _child_dirs(path: Path) -> list[Path]: + """List the immediate subdirectories of a directory, skipping plain files.""" + return [child for child in path.iterdir() if child.is_dir()] + + def _is_job_folder(path: Path) -> bool: """Check if a directory looks like a job folder (contains trial dirs with config.json).""" if not path.is_dir(): @@ -249,20 +254,14 @@ def find_job_folders(artifacts_dir: Path) -> list[Path]: # Check for direct jobs/ folder direct_jobs = artifacts_dir / "jobs" if direct_jobs.exists(): - for item in direct_jobs.iterdir(): - if item.is_dir(): - job_folders.append(item) + job_folders.extend(_child_dirs(direct_jobs)) return job_folders # Check for per-artifact structure - for artifact_dir in artifacts_dir.iterdir(): - if not artifact_dir.is_dir(): - continue + for artifact_dir in _child_dirs(artifacts_dir): jobs_dir = artifact_dir / "jobs" if jobs_dir.exists(): - for item in jobs_dir.iterdir(): - if item.is_dir(): - job_folders.append(item) + job_folders.extend(_child_dirs(jobs_dir)) return job_folders diff --git a/scripts/checkMacAttachFileRuntime.ts b/scripts/checkMacAttachFileRuntime.ts index 1d9649f6f84..1d83af2897e 100644 --- a/scripts/checkMacAttachFileRuntime.ts +++ b/scripts/checkMacAttachFileRuntime.ts @@ -64,15 +64,9 @@ async function findAppBundles(rootDir: string): Promise<{ matches: string[]; see return { matches, seen }; } -async function chooseDefaultAppBundle(): Promise { - const { matches: appBundles, seen } = await findAppBundles(RELEASE_DIR); - assert( - appBundles.length > 0, - `No ${APP_NAME} found under ${RELEASE_DIR}. Run make dist-mac first. Stored .app names: ${ - seen.length > 0 ? seen.join(", ") : "(none)" - }` - ); - +// Takes the already-discovered bundles so callers do not re-walk the release +// tree (and re-run the identical "no bundle found" assert) just to pick one. +function chooseDefaultAppBundle(appBundles: readonly string[]): string { const preferredSuffixes = process.arch === "arm64" ? [ @@ -94,7 +88,7 @@ async function chooseDefaultAppBundle(): Promise { } } - return appBundles.sort()[0]!; + return [...appBundles].sort()[0]!; } async function findFileMatching(rootDir: string, pattern: RegExp): Promise { @@ -304,7 +298,7 @@ async function main(): Promise { }` ); appBundles = matches; - smokeAppBundle = await chooseDefaultAppBundle(); + smokeAppBundle = chooseDefaultAppBundle(matches); } const verifiedArchitectures = new Set(); diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh index e9e92902d50..193becf4dc7 100755 --- a/scripts/postinstall.sh +++ b/scripts/postinstall.sh @@ -81,40 +81,39 @@ else exit 0 fi -# 6) Rebuild node-pty (once per version/platform) -if [ "$HAS_NODE_PTY" = "1" ]; then - if [ -f "$NODE_PTY_STAMP_FILE" ]; then - echo "โœ… node-pty already rebuilt for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH} โ€“ skipping" - else - echo "๐Ÿ”ง Rebuilding node-pty for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH}..." - $REBUILD_CMD @electron/rebuild -f -m node_modules/node-pty || { - echo "โš ๏ธ Failed to rebuild native modules" - echo " Terminal functionality may not work in desktop mode." - echo " Run 'make rebuild-native' manually to fix." - exit 0 - } - touch "$NODE_PTY_STAMP_FILE" - echo "โœ… node-pty rebuilt successfully (cached at $NODE_PTY_STAMP_FILE)" +# 6) Rebuild one native module for Electron's ABI, skipping when its stamp exists. +# A rebuild failure is non-fatal (desktop terminal/DB features degrade, install succeeds), +# so the failure path exits the whole script with 0 rather than returning to the caller. +rebuild_native_module() { + label="$1" + module_path="$2" + stamp_file="$3" + + if [ -f "$stamp_file" ]; then + echo "โœ… ${label} already rebuilt for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH} โ€“ skipping" + return 0 fi + + echo "๐Ÿ”ง Rebuilding ${label} for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH}..." + $REBUILD_CMD @electron/rebuild -f -m "$module_path" || { + echo "โš ๏ธ Failed to rebuild native modules" + echo " Terminal functionality may not work in desktop mode." + echo " Run 'make rebuild-native' manually to fix." + exit 0 + } + touch "$stamp_file" + echo "โœ… ${label} rebuilt successfully (cached at $stamp_file)" +} + +# 7) Rebuild native modules (once per version/platform) +if [ "$HAS_NODE_PTY" = "1" ]; then + rebuild_native_module "node-pty" "node_modules/node-pty" "$NODE_PTY_STAMP_FILE" else echo "โ„น๏ธ node-pty package missing โ€“ skipping node-pty rebuild" fi -# 7) Rebuild DuckDB (once per version/platform) if [ "$HAS_DUCKDB" = "1" ]; then - if [ -f "$DUCKDB_STAMP_FILE" ]; then - echo "โœ… DuckDB already rebuilt for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH} โ€“ skipping" - else - echo "๐Ÿ”ง Rebuilding DuckDB for Electron ${ELECTRON_VERSION} on ${PLATFORM}/${ARCH}..." - $REBUILD_CMD @electron/rebuild -f -m node_modules/@duckdb/node-bindings || { - echo "โš ๏ธ Failed to rebuild native modules" - echo " Terminal functionality may not work in desktop mode." - echo " Run 'make rebuild-native' manually to fix." - exit 0 - } - touch "$DUCKDB_STAMP_FILE" - echo "โœ… DuckDB rebuilt successfully (cached at $DUCKDB_STAMP_FILE)" - fi + rebuild_native_module "DuckDB" "node_modules/@duckdb/node-bindings" "$DUCKDB_STAMP_FILE" else echo "โ„น๏ธ DuckDB packages missing โ€“ skipping DuckDB rebuild" fi diff --git a/src/browser/App.tsx b/src/browser/App.tsx index f599e83d551..296a8b866b6 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -45,6 +45,7 @@ import { LEFT_SIDEBAR_DEFAULT_WIDTH_PX, LEFT_SIDEBAR_MAX_WIDTH_PX, LEFT_SIDEBAR_MIN_WIDTH_PX, + MOBILE_TOUCH_MEDIA_QUERY, } from "@/constants/layout"; import { XUM_PRODUCT_SLUG } from "@/common/constants/product"; import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources"; @@ -233,8 +234,7 @@ function AppInner() { // because the sidebar width is controlled by CSS and shouldn't rewrite the user's desktop // width preference. const isMobileTouch = - typeof window !== "undefined" && - window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches; + typeof window !== "undefined" && window.matchMedia(MOBILE_TOUCH_MEDIA_QUERY).matches; if (isMobileTouch) { return Number.POSITIVE_INFINITY; } diff --git a/src/browser/components/AgentListItem/AgentListItem.tsx b/src/browser/components/AgentListItem/AgentListItem.tsx index 3b3cea3fb29..d23f9c5f41e 100644 --- a/src/browser/components/AgentListItem/AgentListItem.tsx +++ b/src/browser/components/AgentListItem/AgentListItem.tsx @@ -218,8 +218,13 @@ function HeartbeatFallbackIcon() { ); } +/** Plural "s" for the sidebar activity counts, which are all regular nouns. */ +function pluralSuffix(count: number): string { + return count === 1 ? "" : "s"; +} + function formatSubAgentCount(count: number, label: "active" | "queued"): string { - return `${count} sub-agent${count === 1 ? "" : "s"} ${label}`; + return `${count} sub-agent${pluralSuffix(count)} ${label}`; } function formatDelegatedActivityText(activity: WorkspaceDelegatedActivity): string | null { @@ -295,8 +300,7 @@ function formatHiddenSubAgentsPresentation( ? summary.runningWorkflowAgentCount : summary.queuedWorkflowAgentCount; // Gap-only runs have no countable workers; skip the "(0 agents)" noise. - const agentSuffix = - agentCount > 0 ? ` (${agentCount} agent${agentCount === 1 ? "" : "s"})` : ""; + const agentSuffix = agentCount > 0 ? ` (${agentCount} agent${pluralSuffix(agentCount)})` : ""; const queuedSuffix = hasRunningRun && summary.queuedWorkflowAgentCount > 0 ? ` ยท ${summary.queuedWorkflowAgentCount} queued` diff --git a/src/browser/components/ArchivedWorkspaces/ArchivedWorkspaces.tsx b/src/browser/components/ArchivedWorkspaces/ArchivedWorkspaces.tsx index bd94c4b24dd..69440d22bc4 100644 --- a/src/browser/components/ArchivedWorkspaces/ArchivedWorkspaces.tsx +++ b/src/browser/components/ArchivedWorkspaces/ArchivedWorkspaces.tsx @@ -8,7 +8,7 @@ import { getErrorMessage } from "@/common/utils/errors"; import { useAPI } from "@/browser/contexts/API"; import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; -import { usePopoverError } from "@/browser/hooks/usePopoverError"; +import { resolvePopoverErrorAnchor, usePopoverError } from "@/browser/hooks/usePopoverError"; import { ChevronDown, ChevronRight, FolderX, Loader2, Search, Trash2 } from "lucide-react"; import { ArchiveIcon, ArchiveRestoreIcon } from "../icons/ArchiveIcon/ArchiveIcon"; import { Tooltip, TooltipTrigger, TooltipContent } from "../Tooltip/Tooltip"; @@ -584,15 +584,11 @@ export const ArchivedWorkspaces: React.FC = ({ return; } - if (anchorEl) { - const rect = anchorEl.getBoundingClientRect(); - unarchiveError.showError(workspaceId, result.error ?? "Failed to restore workspace", { - top: rect.top + window.scrollY, - left: rect.right + 10, - }); - } else { - unarchiveError.showError(workspaceId, result.error ?? "Failed to restore workspace"); - } + unarchiveError.showError( + workspaceId, + result.error ?? "Failed to restore workspace", + resolvePopoverErrorAnchor(anchorEl) + ); } finally { setProcessingIds((prev) => { const next = new Set(prev); @@ -675,8 +671,7 @@ export const ArchivedWorkspaces: React.FC = ({ }; const handleDeleteWorktree = async (workspaceId: string, anchorEl?: HTMLElement) => { - const rect = anchorEl?.getBoundingClientRect(); - const anchor = rect ? { top: rect.top + window.scrollY, left: rect.right + 10 } : undefined; + const anchor = resolvePopoverErrorAnchor(anchorEl); if (!api) { deleteWorktreeError.showError(workspaceId, "Not connected to server", anchor); diff --git a/src/browser/components/ChatPane/ChatPane.tsx b/src/browser/components/ChatPane/ChatPane.tsx index f1c333bd245..495af9787fa 100644 --- a/src/browser/components/ChatPane/ChatPane.tsx +++ b/src/browser/components/ChatPane/ChatPane.tsx @@ -41,6 +41,7 @@ import { mergeConsecutiveStreamErrors, computeBashOutputGroupInfos, shouldBypassDeferredMessages, + isBashMonitorWakeMessage, } from "@/browser/utils/messages/messageUtils"; import { computeTaskReportLinking } from "@/browser/utils/messages/taskReportLinking"; import { BashCollapsedSummaryModeProvider } from "@/browser/features/Tools/BashCollapsedSummaryModeContext"; @@ -739,8 +740,6 @@ const ChatPaneContent: React.FC = (props) => { [handleScrollContainerKeyDown, isComposerDockEvent] ); - const handleJumpToBottom = jumpToBottom; - // Handler to navigate (scroll) to a specific message by historyId const handleNavigateToMessage = useCallback( (historyId: string) => { @@ -767,7 +766,7 @@ const ChatPaneContent: React.FC = (props) => { // interrupt navigation between human prompts (payloads themselves are assistant rows). if ( message.type === "user" && - message.bashMonitorWake == null && + !isBashMonitorWakeMessage(message) && message.agentPeerMessageTrigger == null ) { userHistoryIds.push(message.historyId); @@ -1012,8 +1011,8 @@ const ChatPaneContent: React.FC = (props) => { // send success can be too late because the backend may not resolve until the // stream has already produced rows, leaving the first deltas offscreen when the // user had previously scrolled up. - handleJumpToBottom(); - }, [handleJumpToBottom]); + jumpToBottom(); + }, [jumpToBottom]); const handleMessageSent = useCallback( (dispatchMode: QueueDispatchMode = "tool-end") => { @@ -1026,15 +1025,15 @@ const ChatPaneContent: React.FC = (props) => { // Slash-command send paths still report after backend success; keep this // harmless duplicate pin so those paths also re-arm auto-scroll. - handleJumpToBottom(); + jumpToBottom(); }, - [autoBackgroundOnSend, handleJumpToBottom] + [autoBackgroundOnSend, jumpToBottom] ); const handleClearHistory = useCallback( async (percentage = 1.0) => { // Re-arm the tail before clearing so the empty/starting state owns the bottom. - handleJumpToBottom(); + jumpToBottom(); // Truncate history in backend const result = await api?.workspace.truncateHistory({ workspaceId, percentage }); @@ -1046,18 +1045,18 @@ const ChatPaneContent: React.FC = (props) => { throw new Error(result.error); } }, - [workspaceId, handleJumpToBottom, api] + [workspaceId, jumpToBottom, api] ); const handleResetContext = useCallback(async (): Promise<"reset" | "noop"> => { - handleJumpToBottom(); + jumpToBottom(); const result = await api?.workspace.resetContext({ workspaceId }); if (!result?.success) { throw new Error(result?.error ?? "Failed to reset context"); } return result.data; - }, [workspaceId, handleJumpToBottom, api]); + }, [workspaceId, jumpToBottom, api]); const openInEditor = useOpenInEditor(); const handleOpenInEditor = useCallback(() => { @@ -1076,8 +1075,8 @@ const ChatPaneContent: React.FC = (props) => { // the ref-backed auto-scroll flag and pins any cached rows before paint; if rows are still // hydrating, the next content resize owns the tail instead of showing the prior workspace's state. useLayoutEffect(() => { - handleJumpToBottom(); - }, [hasLoadedTranscriptRows, handleJumpToBottom, workspaceId]); + jumpToBottom(); + }, [hasLoadedTranscriptRows, jumpToBottom, workspaceId]); // Compute showRetryBarrier once for both keybinds and UI. // Track if last message was interrupted or errored (for RetryBarrier). @@ -1232,7 +1231,7 @@ const ChatPaneContent: React.FC = (props) => { (workspaceState?.canInterrupt ?? false) || (workspaceState?.isStreamStarting ?? false), showRetryBarrier, chatInputAPI, - jumpToBottom: handleJumpToBottom, + jumpToBottom, loadOlderHistory: shouldRenderLoadOlderMessagesButton ? handleLoadOlderHistory : null, handleOpenTerminal: onOpenTerminal, handleOpenInEditor, @@ -1677,7 +1676,7 @@ const ChatPaneContent: React.FC = (props) => { > {!autoScroll && ( - {props.run.events.map((event) => - isRuleKind(getTimelineEventKind(event)) ? ( - - ) : ( - - ) - )} + {props.run.events.map((event) => ( + + ))} ); } @@ -881,18 +887,8 @@ export function TimelinePanelView(props: TimelinePanelViewProps) { /> ); } - if (isRuleKind(getTimelineEventKind(item))) { - return ( - - ); - } return ( -