Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
8bb1526
πŸ€– refactor: dedupe memory sweep recordUsage callbacks
mux-bot[bot] Jul 8, 2026
cb95f8a
πŸ€– refactor: dedupe memory scope-full cap check into helper
mux-bot[bot] Jul 9, 2026
e34660b
refactor: dedupe blockquote line formatting in bash monitor wake prompt
mux-bot[bot] Jul 9, 2026
f5ec654
refactor: dedupe tool_search removal in prepareToolSearch
mux-bot[bot] Jul 9, 2026
0bb109a
refactor: dedupe anthropic cache-create token extraction in usageHelpers
mux-bot[bot] Jul 10, 2026
8923c66
refactor: dedupe capability-model thinking policy resolution
mux-bot[bot] Jul 10, 2026
2010985
refactor: dedupe queue entry clear-callback projection in MessageQueue
mux-bot[bot] Jul 11, 2026
ff08214
refactor: dedupe OpenAI-origin model check in cacheStrategy
mux-bot[bot] Jul 12, 2026
dc37b2c
refactor: dedupe tool-call-execution-start emit in StreamManager
mux-bot[bot] Jul 13, 2026
2085dc9
refactor: dedupe model-parameter extras merge in aiService
mux-bot[bot] Jul 13, 2026
51bc2c6
refactor: unify legacy tool_search part rename helper in toolCatalog
mux-bot[bot] Jul 14, 2026
6c52668
refactor: drop duplicated context-cap rationale comment in codexOAuth
mux-bot[bot] Jul 14, 2026
ef9ed00
refactor: dedupe flat-section pinned block resolution in pinnedReorder
mux-bot[bot] Jul 15, 2026
027a978
refactor: dedupe JSON-wrapped tool-output unwrap in workflowRunMessages
mux-bot[bot] Jul 15, 2026
39e8ffc
refactor: hoist errorType local in finalizeWorkspaceTurnFromStreamError
mux-bot[bot] Jul 15, 2026
c6e5766
refactor: extract buildSkillDescriptor helper for skill discovery
mux-bot[bot] Jul 15, 2026
5e6ea86
refactor: hoist duplicated Date.parse(createdAt) in bash monitor deli…
mux-bot[bot] Jul 16, 2026
4f3c0b9
refactor: extract awaitPendingLoad helper in DevToolsService
mux-bot[bot] Jul 16, 2026
898886f
refactor: dedupe MCP OAuth redirect URI resolution in router
mux-bot[bot] Jul 18, 2026
391d35b
refactor: extract getTotalTokens helper for total-token sums
mux-bot[bot] Jul 20, 2026
3bb2ab5
refactor: hoist duplicated dedupeKeys snapshot in removeByDedupeKeyPr…
mux-bot[bot] Jul 20, 2026
b1eae97
refactor: dedupe settled workspace-turn reconciliation guard
mux-bot[bot] Jul 21, 2026
590d40a
refactor: drop duplicated Kimi K3 max-effort rationale in providerOpt…
mux-bot[bot] Jul 22, 2026
1430c74
refactor: drop redundant structuredOutput guard at subagent report ca…
mux-bot[bot] Jul 22, 2026
16d4e50
refactor: extract isZipMediaType helper for staged attachment media-t…
mux-bot[bot] Jul 23, 2026
18713f3
refactor: hoist duplicated goal-bypass attachment check in ChatInput
mux-bot[bot] Jul 24, 2026
494a78d
refactor: dedupe anchored Anthropic model-id regex construction
mux-bot[bot] Jul 24, 2026
987ab28
refactor: dedupe MCP header telemetry flag derivation in router
mux-bot[bot] Jul 25, 2026
10d0112
refactor: extract _child_dirs helper for job folder discovery
mux-bot[bot] Jul 25, 2026
1b5a946
refactor: drop redundant "exec" fallback duplication for normalizeAge…
mux-bot[bot] Jul 27, 2026
d9f411e
refactor: dedupe defensive unknown-field reads in the timeline mapper
mux-bot[bot] Jul 29, 2026
b534d8b
refactor: share the mobile-touch media query constant
mux-bot[bot] Jul 29, 2026
d6d2818
refactor: share the docked toast overlay placement class
mux-bot[bot] Jul 29, 2026
d6bc05c
refactor: share the primary mouse button guard
mux-bot[bot] Jul 29, 2026
f89232c
refactor: name ModelSelector row selection/highlight state
mux-bot[bot] Jul 30, 2026
0a78cfd
refactor: share the workspace footer pill class
mux-bot[bot] Jul 30, 2026
0501dea
refactor: share the safe inactive-animation pause install
mux-bot[bot] Aug 1, 2026
06134a1
refactor: share the bash monitor wake message predicate
mux-bot[bot] Aug 1, 2026
37ff94d
refactor: dedupe monitor disposition branch in terminate()
mux-bot[bot] Aug 1, 2026
9cf05f1
refactor: extract parseSubagentReportFromMessage helper
mux-bot[bot] Aug 2, 2026
46c3402
refactor: drop stale userOverridable references from experiments UI
mux-bot[bot] Aug 3, 2026
8a84d26
refactor: reuse isTaskAwaitMessage predicate in transcript projection
mux-bot[bot] Aug 3, 2026
93b2914
refactor: dedupe non-blank string coercion in taskReportLinking
mux-bot[bot] Aug 4, 2026
a98cbb3
refactor: drop redundant agentType re-check in task_await rows
mux-bot[bot] Aug 5, 2026
05467c9
refactor: hoist duplicated isGrok45Model check in xAI model creation
mux-bot[bot] Aug 6, 2026
5258781
refactor: share the bash-monitor-wake metadata type guard
mux-bot[bot] Aug 6, 2026
1fb715c
refactor: share the persisted tool-error message extraction
mux-bot[bot] Aug 7, 2026
55fe928
refactor: share the terminal-attention message text join
mux-bot[bot] Aug 7, 2026
098771a
refactor: share the agent-plugin error diagnostic push
mux-bot[bot] Aug 7, 2026
77e1bfd
refactor: extract projectSelectOptions helper for creation project pi…
mux-bot[bot] Aug 8, 2026
7fe73c7
refactor: extract shared raw-tool wrapper for MCP startup paths
mux-bot[bot] Aug 9, 2026
f298340
refactor: extract createExecError for the three exec failure paths
mux-bot[bot] Aug 10, 2026
96c186a
refactor: dedupe failed project-result construction in applyProjectPatch
Aug 10, 2026
828eb89
refactor: dedupe popover error anchor math into resolvePopoverErrorAn…
mux-bot[bot] Aug 12, 2026
91958c1
refactor: dedupe the notification settings panel in WorkspaceMenuBar
mux-bot[bot] Aug 12, 2026
5d54d97
refactor: dedupe persisted-choice normalization in AnalyticsDashboard
mux-bot[bot] Aug 12, 2026
f5a12ec
refactor: extract mergeFetchHeaders for provider fetch wrappers
mux-bot[bot] Aug 12, 2026
897c81e
refactor: dedupe unknown-to-record narrowing in PRStatusStore
mux-bot[bot] Aug 12, 2026
68f038b
refactor: dedupe native-xhigh label branches in getThinkingDisplayLabel
mux-bot[bot] Aug 12, 2026
9ee27c6
refactor: extract rebuild_native_module helper in postinstall.sh
mux-bot[bot] Aug 13, 2026
6e110f0
refactor: dedupe Gemini Flash thinking-level tier prefixes
mux-bot[bot] Aug 13, 2026
93e53d0
refactor: dedupe the task-group running-work predicate
mux-bot[bot] Aug 14, 2026
8c21912
refactor: dedupe plural-suffix logic in sidebar activity labels
mux-bot[bot] Aug 14, 2026
5f99d81
refactor: extract splitCoderGatewayModelId helper
mux-bot[bot] Aug 15, 2026
07d770e
refactor: dedupe timeline rule-vs-event row dispatch
mux-bot[bot] Aug 16, 2026
8a43cdb
refactor: extract buildCommandReplacement for slash suggestion traili…
mux-bot[bot] Aug 16, 2026
82af7dc
refactor: dedupe the pending patch-artifact seed in generate()
Aug 16, 2026
e50d9e0
refactor: dedupe blob content-address computation in BlobStore
mux-bot[bot] Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions benchmarks/terminal_bench/prepare_leaderboard_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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

Expand Down
55 changes: 27 additions & 28 deletions scripts/postinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";

Expand Down Expand Up @@ -231,8 +232,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;
}
Expand Down
10 changes: 7 additions & 3 deletions src/browser/components/AgentListItem/AgentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`
Expand Down
19 changes: 7 additions & 12 deletions src/browser/components/ArchivedWorkspaces/ArchivedWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -584,15 +584,11 @@ export const ArchivedWorkspaces: React.FC<ArchivedWorkspacesProps> = ({
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);
Expand Down Expand Up @@ -675,8 +671,7 @@ export const ArchivedWorkspaces: React.FC<ArchivedWorkspacesProps> = ({
};

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);
Expand Down
3 changes: 2 additions & 1 deletion src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -764,7 +765,7 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
const userHistoryIds: string[] = [];
for (const message of deferredMessages) {
// Monitor wake events should not interrupt navigation between human prompts.
if (message.type === "user" && message.bashMonitorWake == null) {
if (message.type === "user" && !isBashMonitorWakeMessage(message)) {
userHistoryIds.push(message.historyId);
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/browser/components/ChatPane/WorkspaceFooterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ function WorkspaceBranchControls(props: {
);
}

// Shared by the footer's interactive pills (repository link, "Last prompt") so they keep reading as
// one affordance family: restyling one silently drifting from the other is the failure mode here.
const FOOTER_PILL_CLASS =
"text-muted hover:bg-hover hover:text-foreground focus-visible:ring-accent flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 transition-colors focus-visible:ring-1";

function FooterRepositoryLabel(props: { workspaceId: string; projectLabel: string }) {
const workspacePR = useWorkspacePR(props.workspaceId);

Expand All @@ -169,7 +174,7 @@ function FooterRepositoryLabel(props: { workspaceId: string; projectLabel: strin
target="_blank"
rel="noopener noreferrer"
data-testid="workspace-footer-repository"
className="text-muted hover:bg-hover hover:text-foreground focus-visible:ring-accent flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 transition-colors focus-visible:ring-1"
className={FOOTER_PILL_CLASS}
>
<Github className="h-3 w-3 shrink-0" aria-hidden="true" />
<span className="font-mono">{slug}</span>
Expand Down Expand Up @@ -320,7 +325,8 @@ function FooterLastPrompt(props: { workspaceId: string }) {
<PopoverTrigger asChild>
<button
type="button"
className="text-muted hover:bg-hover hover:text-foreground focus-visible:ring-accent flex h-5 shrink-0 cursor-pointer items-center gap-1 rounded-md border-0 bg-transparent px-1.5 transition-colors focus-visible:ring-1"
// The extra classes are <button> resets; the pill styling itself is shared.
className={cn(FOOTER_PILL_CLASS, "cursor-pointer border-0 bg-transparent")}
data-testid="workspace-footer-last-prompt"
onKeyDown={stopKeyboardPropagation}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from "react";
import { useAPI } from "@/browser/contexts/API";

const wrapperClassName =
"pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto";
import { CHAT_DOCK_TOAST_OVERLAY_CLASS } from "@/constants/layout";

/**
* Connection status banner that uses the same *overlay placement* as ChatInputToast.
Expand Down Expand Up @@ -55,7 +53,7 @@ export const ConnectionStatusToast: React.FC<ConnectionStatusToastProps> = ({ wr

if (!wrap) return content;

return <div className={wrapperClassName}>{content}</div>;
return <div className={CHAT_DOCK_TOAST_OVERLAY_CLASS}>{content}</div>;
}

if (apiState.status === "error") {
Expand All @@ -75,7 +73,7 @@ export const ConnectionStatusToast: React.FC<ConnectionStatusToastProps> = ({ wr

if (!wrap) return content;

return <div className={wrapperClassName}>{content}</div>;
return <div className={CHAT_DOCK_TOAST_OVERLAY_CLASS}>{content}</div>;
}

return null;
Expand Down
17 changes: 9 additions & 8 deletions src/browser/components/ModelSelector/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,33 +406,34 @@ export const ModelSelector = forwardRef<ModelSelectorRef, ModelSelectorProps>(
const modelProvider = getModelProvider(model);
const showProviderLabel =
modelProvider.length > 0 && duplicateModelNames.has(modelName);
// Name the row's selection/highlight state once so the option class, ARIA state,
// and accent styling below can't drift apart (mirrors AgentModePicker).
const isHighlighted = index === highlightedIndex;
const isSelected = value === model;

return (
<div
key={model}
data-highlighted={index === highlightedIndex}
data-highlighted={isHighlighted}
onMouseEnter={() => setHighlightedIndex(index)}
className={composerPickerOptionClass(
{
isHighlighted: index === highlightedIndex,
isSelected: value === model,
},
{ isHighlighted, isSelected },
"py-1",
hiddenSet.has(model) && "opacity-50"
)}
onClick={() => handleSelectModel(model)}
role="option"
aria-selected={value === model}
aria-selected={isSelected}
>
<ProviderIcon
provider={modelProvider}
className={cn(
"h-3 w-3 shrink-0",
value === model ? "text-accent" : "text-muted"
isSelected ? "text-accent" : "text-muted"
)}
/>
<span className="flex min-w-0 flex-1 items-baseline gap-1">
<span className={cn("min-w-0 truncate", value === model && "text-accent")}>
<span className={cn("min-w-0 truncate", isSelected && "text-accent")}>
{formatModelDisplayName(modelName)}
</span>
{showProviderLabel && (
Expand Down
40 changes: 5 additions & 35 deletions src/browser/components/ProjectSidebar/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ import {
} from "lucide-react";
import { useWorkspaceActions } from "@/browser/contexts/WorkspaceContext";
import { useRouter } from "@/browser/contexts/RouterContext";
import { usePopoverError } from "@/browser/hooks/usePopoverError";
import { resolvePopoverErrorAnchor, usePopoverError } from "@/browser/hooks/usePopoverError";
import { forkWorkspace } from "@/browser/utils/chatCommands";
import { PopoverError } from "../PopoverError/PopoverError";
import { SectionHeader } from "../SectionHeader/SectionHeader";
Expand Down Expand Up @@ -1016,14 +1016,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
return;
}

let anchor: { top: number; left: number } | undefined;
if (buttonElement) {
const rect = buttonElement.getBoundingClientRect();
anchor = {
top: rect.top + window.scrollY,
left: rect.right + 10,
};
}
const anchor = resolvePopoverErrorAnchor(buttonElement);

try {
const result = await forkWorkspace({
Expand All @@ -1045,14 +1038,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({

const handleStopRuntime = useCallback(
async (workspaceId: string, buttonElement?: HTMLElement) => {
let anchor: { top: number; left: number } | undefined;
if (buttonElement) {
const rect = buttonElement.getBoundingClientRect();
anchor = {
top: rect.top + window.scrollY,
left: rect.right + 10,
};
}
const anchor = resolvePopoverErrorAnchor(buttonElement);

if (!api) {
workspaceStopRuntimeError.showError(workspaceId, "Not connected to server", anchor);
Expand Down Expand Up @@ -1305,14 +1291,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
message = error.message ?? "Failed to remove project";
}

let anchor: { top: number; left: number } | undefined;
if (buttonElement) {
const rect = buttonElement.getBoundingClientRect();
anchor = {
top: rect.top + window.scrollY,
left: rect.right + 10,
};
}
const anchor = resolvePopoverErrorAnchor(buttonElement);

projectRemoveError.showError(projectPath, message, anchor);
},
Expand Down Expand Up @@ -1377,16 +1356,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
) => {
// Capture the anchor location up front because the sub-project action menu unmounts its
// button immediately after click; failures still need stable error placement.
const anchor =
buttonElement != null
? (() => {
const buttonRect = buttonElement.getBoundingClientRect();
return {
top: buttonRect.top + window.scrollY,
left: buttonRect.right + 10,
};
})()
: undefined;
const anchor = resolvePopoverErrorAnchor(buttonElement);

// Removing a sub-project unregisters it and clears the cwd pointer from its workspaces.
const workspacesInSection = (userProjects.get(projectPath)?.workspaces ?? []).filter(
Expand Down
Loading
Loading