Make workspace tab switching faster (#1251)
* Avoid workspace shell renders when toggling sidebar * Avoid rerendering inactive workspace screens * Avoid rerendering unchanged sidebar rows * Keep sidebar selection state in the list * Speed up workspace tab switching
This commit is contained in:
@@ -43,6 +43,70 @@ In any worktree-style or portless setup, never assume default ports.
|
|||||||
`http://127.0.0.1:9223` so renderer CPU profiles can be captured through CDP.
|
`http://127.0.0.1:9223` so renderer CPU profiles can be captured through CDP.
|
||||||
Override the port with `PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when `9223` is busy.
|
Override the port with `PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when `9223` is busy.
|
||||||
|
|
||||||
|
### React render profiling
|
||||||
|
|
||||||
|
The app has a gated React render profiler in
|
||||||
|
`packages/app/src/utils/render-profiler.tsx`. Wrap the component boundary you want
|
||||||
|
to measure with `RenderProfile`, then open the app with `?renderProfile=1`. When
|
||||||
|
the query param is absent, `RenderProfile` returns children directly and records
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
Captured samples are exposed on `globalThis.__PASEO_RENDER_PROFILE__`. Call
|
||||||
|
`globalThis.__PASEO_RESET_RENDER_PROFILE__?.()` after warm-up and before the
|
||||||
|
interaction you want to measure. If a memo comparator or subscription boundary
|
||||||
|
needs explanation, call `recordRenderProfileReasons(id, reasons)` while profiling;
|
||||||
|
reason counts are exposed on `globalThis.__PASEO_RENDER_PROFILE_REASONS__`.
|
||||||
|
|
||||||
|
Use this workflow for any render investigation:
|
||||||
|
|
||||||
|
1. Add stable `RenderProfile` boundaries around the suspected root and expensive
|
||||||
|
children. Keep IDs specific enough to compare before and after.
|
||||||
|
2. Reproduce against real app state, not toy fixtures, whenever practical.
|
||||||
|
3. Record an idle baseline first. If idle is noisy, fix or account for that
|
||||||
|
before optimizing the interaction.
|
||||||
|
4. Warm up the route, reset profiler samples, run the exact interaction, then
|
||||||
|
compare `actualDuration`, render counts, and per-commit samples.
|
||||||
|
5. When a memo boundary still renders, record reasons before changing code. Do
|
||||||
|
not guess from object identity alone.
|
||||||
|
6. Keep changes that move the measured profile. Remove probes or memo wrappers
|
||||||
|
that do not move the number.
|
||||||
|
|
||||||
|
What this caught during the workspace tab investigation:
|
||||||
|
|
||||||
|
- A large apparent workspace cost was real interaction work, not daemon noise;
|
||||||
|
the idle baseline stayed near zero.
|
||||||
|
- The expensive stream rerender was mostly prop identity churn from pane context
|
||||||
|
callbacks and capability objects, not new stream data.
|
||||||
|
- Stabilizing provider actions at the pane boundary helped because every mounted
|
||||||
|
panel consumes that context.
|
||||||
|
- Comparing value-shaped capability flags beat preserving object identity through
|
||||||
|
unrelated stores.
|
||||||
|
- Some plausible fixes did not pay off: memoizing the tab row and composer draft
|
||||||
|
object barely moved the profile, so they were removed.
|
||||||
|
|
||||||
|
Existing scenario script: workspace agent/terminal tab switching. Start Expo on
|
||||||
|
web, keep a daemon available, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PASEO_PROFILE_SERVER_ID=<server-id> \
|
||||||
|
PASEO_PROFILE_WORKSPACE_ID=<workspace-path> \
|
||||||
|
PASEO_PROFILE_AGENT_ID=<agent-id> \
|
||||||
|
npm run profile:workspace-tabs --workspace=@getpaseo/app
|
||||||
|
```
|
||||||
|
|
||||||
|
This script opens the app with `?renderProfile=1`, creates a temporary terminal
|
||||||
|
tab, switches between a real agent and that terminal, prints aggregated React
|
||||||
|
Profiler timings, then removes the temporary terminal. It is an example of the
|
||||||
|
workflow above, not the only way to use the profiler. Useful knobs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PASEO_PROFILE_APP_URL=http://localhost:19010 # Expo web URL
|
||||||
|
PASEO_PROFILE_SWITCH_COUNT=1 # number of agent/terminal switch pairs
|
||||||
|
PASEO_PROFILE_SWITCH_WAIT_MS=250 # delay after each click
|
||||||
|
PASEO_PROFILE_IDLE_WAIT_MS=3000 # idle baseline before switching
|
||||||
|
PASEO_PROFILE_DUMP_COMMITS=1 # include per-commit profiler samples
|
||||||
|
```
|
||||||
|
|
||||||
### Desktop macOS compositor watchdog
|
### Desktop macOS compositor watchdog
|
||||||
|
|
||||||
macOS display sleep can leave Chromium's GPU-process display link — the vsync
|
macOS display sleep can leave Chromium's GPU-process display link — the vsync
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"build": "npm run build:web",
|
"build": "npm run build:web",
|
||||||
"build:web": "npm --prefix ../.. run build:app-deps && expo export --platform web",
|
"build:web": "npm --prefix ../.. run build:app-deps && expo export --platform web",
|
||||||
|
"profile:workspace-tabs": "node ./scripts/profile-workspace-tabs.mjs",
|
||||||
"deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main",
|
"deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main",
|
||||||
"build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs"
|
"build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
|
||||||
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||||
|
|
||||||
|
const baseUrl = process.env.PASEO_PROFILE_APP_URL ?? "http://localhost:19010";
|
||||||
|
const serverId = requiredEnv("PASEO_PROFILE_SERVER_ID");
|
||||||
|
const workspaceId = requiredEnv("PASEO_PROFILE_WORKSPACE_ID");
|
||||||
|
const agentId = requiredEnv("PASEO_PROFILE_AGENT_ID");
|
||||||
|
const switchCount = Number(process.env.PASEO_PROFILE_SWITCH_COUNT ?? 6);
|
||||||
|
const switchWaitMs = Number(process.env.PASEO_PROFILE_SWITCH_WAIT_MS ?? 250);
|
||||||
|
const idleWaitMs = Number(process.env.PASEO_PROFILE_IDLE_WAIT_MS ?? 0);
|
||||||
|
const dumpCommits = process.env.PASEO_PROFILE_DUMP_COMMITS === "1";
|
||||||
|
|
||||||
|
function requiredEnv(name) {
|
||||||
|
const value = process.env[name]?.trim();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`${name} is required`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTerminal() {
|
||||||
|
const output = execFileSync(
|
||||||
|
"npm",
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"cli",
|
||||||
|
"--",
|
||||||
|
"terminal",
|
||||||
|
"create",
|
||||||
|
"--cwd",
|
||||||
|
workspaceId,
|
||||||
|
"--name",
|
||||||
|
"Tab Switch Profile",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
{ cwd: repoRoot, encoding: "utf8" },
|
||||||
|
);
|
||||||
|
const match = output.match(/\{[\s\S]*\}/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Could not parse terminal create output: ${output}`);
|
||||||
|
}
|
||||||
|
return JSON.parse(match[0]).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killTerminal(terminalId) {
|
||||||
|
execFileSync("npm", ["run", "cli", "--", "terminal", "kill", terminalId], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdTerminalId = process.env.PASEO_PROFILE_TERMINAL_ID ? null : createTerminal();
|
||||||
|
const terminalId = process.env.PASEO_PROFILE_TERMINAL_ID ?? createdTerminalId;
|
||||||
|
const workspaceSegment = `b64_${Buffer.from(workspaceId, "utf8").toString("base64url")}`;
|
||||||
|
const workspaceUrl = `${baseUrl}/h/${serverId}/workspace/${workspaceSegment}`;
|
||||||
|
|
||||||
|
function tabTestId(kind, id) {
|
||||||
|
return `workspace-tab-${kind}_${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(samples) {
|
||||||
|
const byId = new Map();
|
||||||
|
for (const sample of samples) {
|
||||||
|
const current = byId.get(sample.id) ?? {
|
||||||
|
renders: 0,
|
||||||
|
actualDuration: 0,
|
||||||
|
baseDuration: 0,
|
||||||
|
};
|
||||||
|
current.renders += 1;
|
||||||
|
current.actualDuration += sample.actualDuration;
|
||||||
|
current.baseDuration += sample.baseDuration;
|
||||||
|
byId.set(sample.id, current);
|
||||||
|
}
|
||||||
|
return [...byId.entries()]
|
||||||
|
.map(([id, value]) => Object.assign({ id }, value))
|
||||||
|
.sort((left, right) => right.actualDuration - left.actualDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeCommits(samples) {
|
||||||
|
const byCommit = new Map();
|
||||||
|
for (const sample of samples) {
|
||||||
|
const key = String(sample.commitTime);
|
||||||
|
const current = byCommit.get(key) ?? {
|
||||||
|
commitTime: sample.commitTime,
|
||||||
|
totalActualDuration: 0,
|
||||||
|
samples: [],
|
||||||
|
};
|
||||||
|
current.totalActualDuration += sample.actualDuration;
|
||||||
|
current.samples.push({
|
||||||
|
id: sample.id,
|
||||||
|
actualDuration: sample.actualDuration,
|
||||||
|
baseDuration: sample.baseDuration,
|
||||||
|
});
|
||||||
|
byCommit.set(key, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...byCommit.values()].sort((left, right) => left.commitTime - right.commitTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openIntent(page, value) {
|
||||||
|
const url = `${workspaceUrl}?renderProfile=1&open=${encodeURIComponent(value)}`;
|
||||||
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||||
|
await page.getByTestId("workspace-tabs-row").waitFor({ timeout: 60_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickTab(page, testId) {
|
||||||
|
await page.getByTestId(testId).click();
|
||||||
|
await page.waitForTimeout(switchWaitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openIntent(page, `agent:${agentId}`);
|
||||||
|
await openIntent(page, `terminal:${terminalId}`);
|
||||||
|
|
||||||
|
const agentTab = tabTestId("agent", agentId);
|
||||||
|
const terminalTab = tabTestId("terminal", terminalId);
|
||||||
|
await page.getByTestId(agentTab).waitFor({ timeout: 60_000 });
|
||||||
|
await page.getByTestId(terminalTab).waitFor({ timeout: 60_000 });
|
||||||
|
|
||||||
|
await clickTab(page, agentTab);
|
||||||
|
await clickTab(page, terminalTab);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.evaluate(() => globalThis.__PASEO_RESET_RENDER_PROFILE__?.());
|
||||||
|
|
||||||
|
if (idleWaitMs > 0) {
|
||||||
|
await page.waitForTimeout(idleWaitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < switchCount; index += 1) {
|
||||||
|
await clickTab(page, agentTab);
|
||||||
|
await clickTab(page, terminalTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = await page.evaluate(() => globalThis.__PASEO_RENDER_PROFILE__ ?? []);
|
||||||
|
const reasons = await page.evaluate(() => globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {});
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
appUrl: baseUrl,
|
||||||
|
serverId,
|
||||||
|
workspaceId,
|
||||||
|
agentId,
|
||||||
|
terminalId,
|
||||||
|
switchCount,
|
||||||
|
switchWaitMs,
|
||||||
|
idleWaitMs,
|
||||||
|
samples: samples.length,
|
||||||
|
summary: summarize(samples),
|
||||||
|
reasons,
|
||||||
|
commits: dumpCommits ? summarizeCommits(samples) : undefined,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
if (createdTerminalId) {
|
||||||
|
killTerminal(createdTerminalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -41,6 +41,7 @@ import { PlanCard } from "@/components/plan-card";
|
|||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
import type { PendingPermission } from "@/types/shared";
|
import type { PendingPermission } from "@/types/shared";
|
||||||
import type {
|
import type {
|
||||||
|
AgentCapabilityFlags,
|
||||||
AgentPermissionAction,
|
AgentPermissionAction,
|
||||||
AgentPermissionResponse,
|
AgentPermissionResponse,
|
||||||
} from "@getpaseo/protocol/agent-types";
|
} from "@getpaseo/protocol/agent-types";
|
||||||
@@ -77,6 +78,7 @@ import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
|||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
|
import { recordRenderProfileReasons } from "@/utils/render-profiler";
|
||||||
|
|
||||||
function renderLiveAuxiliaryNode(input: {
|
function renderLiveAuxiliaryNode(input: {
|
||||||
pendingPermissions: ReactNode;
|
pendingPermissions: ReactNode;
|
||||||
@@ -214,6 +216,18 @@ export interface AgentStreamViewProps {
|
|||||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AGENT_CAPABILITY_FLAG_KEYS: (keyof AgentCapabilityFlags)[] = [
|
||||||
|
"supportsStreaming",
|
||||||
|
"supportsSessionPersistence",
|
||||||
|
"supportsDynamicModes",
|
||||||
|
"supportsMcpServers",
|
||||||
|
"supportsReasoningStream",
|
||||||
|
"supportsToolInvocations",
|
||||||
|
"supportsRewindConversation",
|
||||||
|
"supportsRewindFiles",
|
||||||
|
"supportsRewindBoth",
|
||||||
|
];
|
||||||
|
|
||||||
const EMPTY_STREAM_HEAD: StreamItem[] = [];
|
const EMPTY_STREAM_HEAD: StreamItem[] = [];
|
||||||
|
|
||||||
const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(
|
const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(
|
||||||
@@ -745,7 +759,68 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AgentStreamView = memo(AgentStreamViewComponent);
|
function agentCapabilityFlagsEqual(
|
||||||
|
left: AgentCapabilityFlags | undefined,
|
||||||
|
right: AgentCapabilityFlags | undefined,
|
||||||
|
): boolean {
|
||||||
|
return AGENT_CAPABILITY_FLAG_KEYS.every((key) => left?.[key] === right?.[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAgentScreenAgentDiffs(left: AgentScreenAgent, right: AgentScreenAgent): string[] {
|
||||||
|
const reasons: string[] = [];
|
||||||
|
if (left.serverId !== right.serverId) reasons.push("agent.serverId");
|
||||||
|
if (left.id !== right.id) reasons.push("agent.id");
|
||||||
|
if (left.status !== right.status) reasons.push("agent.status");
|
||||||
|
if (left.cwd !== right.cwd) reasons.push("agent.cwd");
|
||||||
|
if (!agentCapabilityFlagsEqual(left.capabilities, right.capabilities)) {
|
||||||
|
reasons.push("agent.capabilities");
|
||||||
|
}
|
||||||
|
if (left.lastError !== right.lastError) reasons.push("agent.lastError");
|
||||||
|
if (left.projectPlacement?.checkout?.cwd !== right.projectPlacement?.checkout?.cwd) {
|
||||||
|
reasons.push("agent.projectPlacement.checkout.cwd");
|
||||||
|
}
|
||||||
|
if (left.projectPlacement?.checkout?.isGit !== right.projectPlacement?.checkout?.isGit) {
|
||||||
|
reasons.push("agent.projectPlacement.checkout.isGit");
|
||||||
|
}
|
||||||
|
return reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bottomAnchorRouteRequestsEqual(
|
||||||
|
left: BottomAnchorRouteRequest | null | undefined,
|
||||||
|
right: BottomAnchorRouteRequest | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
left?.agentId === right?.agentId &&
|
||||||
|
left?.reason === right?.reason &&
|
||||||
|
left?.requestKey === right?.requestKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function agentStreamViewPropsEqual(
|
||||||
|
left: AgentStreamViewProps,
|
||||||
|
right: AgentStreamViewProps,
|
||||||
|
): boolean {
|
||||||
|
const reasons: string[] = [];
|
||||||
|
if (left.agentId !== right.agentId) reasons.push("agentId");
|
||||||
|
if (left.serverId !== right.serverId) reasons.push("serverId");
|
||||||
|
reasons.push(...collectAgentScreenAgentDiffs(left.agent, right.agent));
|
||||||
|
if (left.streamItems !== right.streamItems) reasons.push("streamItems");
|
||||||
|
if (left.pendingPermissions !== right.pendingPermissions) reasons.push("pendingPermissions");
|
||||||
|
if (
|
||||||
|
!bottomAnchorRouteRequestsEqual(left.routeBottomAnchorRequest, right.routeBottomAnchorRequest)
|
||||||
|
) {
|
||||||
|
reasons.push("routeBottomAnchorRequest");
|
||||||
|
}
|
||||||
|
if (left.isAuthoritativeHistoryReady !== right.isAuthoritativeHistoryReady) {
|
||||||
|
reasons.push("isAuthoritativeHistoryReady");
|
||||||
|
}
|
||||||
|
if (left.toast !== right.toast) reasons.push("toast");
|
||||||
|
if (left.onOpenWorkspaceFile !== right.onOpenWorkspaceFile) reasons.push("onOpenWorkspaceFile");
|
||||||
|
recordRenderProfileReasons(`AgentStreamView:${right.agentId}`, reasons);
|
||||||
|
return reasons.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentStreamView = memo(AgentStreamViewComponent, agentStreamViewPropsEqual);
|
||||||
AgentStreamView.displayName = "AgentStreamView";
|
AgentStreamView.displayName = "AgentStreamView";
|
||||||
|
|
||||||
interface ToolCallSlotProps extends Omit<
|
interface ToolCallSlotProps extends Omit<
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, type ReactElement } from "react";
|
import { memo, useCallback, useMemo, useRef, type ReactElement } from "react";
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -43,7 +43,7 @@ interface SortableItemProps<T> {
|
|||||||
useDragHandle: boolean;
|
useDragHandle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortableItem<T>({
|
function SortableItemInner<T>({
|
||||||
id,
|
id,
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
@@ -118,6 +118,8 @@ function SortableItem<T>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SortableItem = memo(SortableItemInner) as typeof SortableItemInner;
|
||||||
|
|
||||||
export function DraggableList<T>({
|
export function DraggableList<T>({
|
||||||
data,
|
data,
|
||||||
keyExtractor,
|
keyExtractor,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ProjectIconView } from "@/components/project-icon-view";
|
|||||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||||
import { invalidateCheckoutGitQueriesForClient } from "@/git/query-keys";
|
import { invalidateCheckoutGitQueriesForClient } from "@/git/query-keys";
|
||||||
import {
|
import {
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
@@ -27,7 +28,11 @@ import {
|
|||||||
type Ref,
|
type Ref,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { router, usePathname, type Href } from "expo-router";
|
import { router, usePathname, type Href } from "expo-router";
|
||||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
import {
|
||||||
|
navigateToWorkspace,
|
||||||
|
useActiveWorkspaceSelection,
|
||||||
|
type ActiveWorkspaceSelection,
|
||||||
|
} from "@/stores/navigation-active-workspace-store";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { type GestureType } from "react-native-gesture-handler";
|
import { type GestureType } from "react-native-gesture-handler";
|
||||||
@@ -101,7 +106,6 @@ import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
|||||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
import type { PrHint } from "@/git/use-pr-status-query";
|
import type { PrHint } from "@/git/use-pr-status-query";
|
||||||
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
|
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
|
||||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
|
||||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||||
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
||||||
@@ -173,6 +177,52 @@ function getPrIconUniMapping(state: PrHint["state"]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useStableProjectIconData(
|
||||||
|
data: (string | null)[],
|
||||||
|
signature: string,
|
||||||
|
): readonly (string | null)[] {
|
||||||
|
const stableRef = useRef<{ signature: string; data: (string | null)[] } | null>(null);
|
||||||
|
if (stableRef.current?.signature !== signature) {
|
||||||
|
stableRef.current = { signature, data };
|
||||||
|
}
|
||||||
|
return stableRef.current.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWorkspaceSelected(input: {
|
||||||
|
selection: ActiveWorkspaceSelection | null;
|
||||||
|
serverId: string | null;
|
||||||
|
workspaceId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return (
|
||||||
|
input.enabled &&
|
||||||
|
input.selection?.serverId === input.serverId &&
|
||||||
|
input.selection.workspaceId === input.workspaceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProjectSelectedByRoute(input: {
|
||||||
|
selection: ActiveWorkspaceSelection | null;
|
||||||
|
project: SidebarProjectEntry;
|
||||||
|
serverId: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return (
|
||||||
|
input.enabled &&
|
||||||
|
input.selection?.serverId === input.serverId &&
|
||||||
|
input.project.workspaces.some(
|
||||||
|
(workspace) => workspace.workspaceId === input.selection?.workspaceId,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionForSelectedWorkspace(
|
||||||
|
selected: boolean,
|
||||||
|
workspace: SidebarWorkspaceEntry,
|
||||||
|
): ActiveWorkspaceSelection | null {
|
||||||
|
return selected ? { serverId: workspace.serverId, workspaceId: workspace.workspaceId } : null;
|
||||||
|
}
|
||||||
|
|
||||||
interface SidebarWorkspaceListProps {
|
interface SidebarWorkspaceListProps {
|
||||||
projects: SidebarProjectEntry[];
|
projects: SidebarProjectEntry[];
|
||||||
serverId: string | null;
|
serverId: string | null;
|
||||||
@@ -1493,7 +1543,6 @@ function WorkspaceRowWithMenu({
|
|||||||
isCreating?: boolean;
|
isCreating?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
|
||||||
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree);
|
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
||||||
@@ -1516,9 +1565,9 @@ function WorkspaceRowWithMenu({
|
|||||||
redirectIfArchivingActiveWorkspace({
|
redirectIfArchivingActiveWorkspace({
|
||||||
serverId: workspace.serverId,
|
serverId: workspace.serverId,
|
||||||
workspaceId: workspace.workspaceId,
|
workspaceId: workspace.workspaceId,
|
||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection: selectionForSelectedWorkspace(selected, workspace),
|
||||||
});
|
});
|
||||||
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
}, [selected, workspace]);
|
||||||
|
|
||||||
const archiveWorktreeAfterConfirmation = useCallback(async () => {
|
const archiveWorktreeAfterConfirmation = useCallback(async () => {
|
||||||
if (isArchiving) {
|
if (isArchiving) {
|
||||||
@@ -1755,15 +1804,14 @@ function NonGitProjectRowWithMenuContent({
|
|||||||
}) {
|
}) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
|
||||||
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
||||||
const redirectAfterArchive = useCallback(() => {
|
const redirectAfterArchive = useCallback(() => {
|
||||||
redirectIfArchivingActiveWorkspace({
|
redirectIfArchivingActiveWorkspace({
|
||||||
serverId: workspace.serverId,
|
serverId: workspace.serverId,
|
||||||
workspaceId: workspace.workspaceId,
|
workspaceId: workspace.workspaceId,
|
||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection: selectionForSelectedWorkspace(selected, workspace),
|
||||||
});
|
});
|
||||||
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
}, [selected, workspace]);
|
||||||
|
|
||||||
const handleArchiveWorkspace = useCallback(() => {
|
const handleArchiveWorkspace = useCallback(() => {
|
||||||
if (isArchivingWorkspace) {
|
if (isArchivingWorkspace) {
|
||||||
@@ -1883,6 +1931,7 @@ function FlattenedProjectRow({
|
|||||||
onRemoveProject,
|
onRemoveProject,
|
||||||
removeProjectStatus,
|
removeProjectStatus,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
|
activeWorkspaceSelection,
|
||||||
}: {
|
}: {
|
||||||
project: SidebarProjectEntry;
|
project: SidebarProjectEntry;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -1901,13 +1950,15 @@ function FlattenedProjectRow({
|
|||||||
onRemoveProject?: () => void;
|
onRemoveProject?: () => void;
|
||||||
removeProjectStatus?: "idle" | "pending";
|
removeProjectStatus?: "idle" | "pending";
|
||||||
selectionEnabled: boolean;
|
selectionEnabled: boolean;
|
||||||
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||||
}) {
|
}) {
|
||||||
const workspace = useSidebarWorkspaceEntry(serverId, rowModel.workspace.workspaceId);
|
const workspace = useSidebarWorkspaceEntry(serverId, rowModel.workspace.workspaceId);
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
const selected = isWorkspaceSelected({
|
||||||
const selected =
|
selection: activeWorkspaceSelection,
|
||||||
selectionEnabled &&
|
serverId,
|
||||||
activeWorkspaceSelection?.serverId === serverId &&
|
workspaceId: rowModel.workspace.workspaceId,
|
||||||
activeWorkspaceSelection.workspaceId === rowModel.workspace.workspaceId;
|
enabled: selectionEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1965,7 +2016,7 @@ interface WorkspaceRowItemProps {
|
|||||||
isCreating?: boolean;
|
isCreating?: boolean;
|
||||||
selectionEnabled: boolean;
|
selectionEnabled: boolean;
|
||||||
serverId: string | null;
|
serverId: string | null;
|
||||||
currentPathname: string | null;
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
drag?: () => void;
|
drag?: () => void;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
@@ -1980,7 +2031,7 @@ function WorkspaceRowItem({
|
|||||||
isCreating = false,
|
isCreating = false,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
serverId,
|
serverId,
|
||||||
currentPathname,
|
activeWorkspaceSelection,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
drag,
|
drag,
|
||||||
isDragging = false,
|
isDragging = false,
|
||||||
@@ -1991,8 +2042,8 @@ function WorkspaceRowItem({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
navigateToWorkspace(serverId, workspace.workspaceId, { currentPathname });
|
navigateToWorkspace(serverId, workspace.workspaceId);
|
||||||
}, [serverId, onWorkspacePress, workspace.workspaceId, currentPathname]);
|
}, [serverId, onWorkspacePress, workspace.workspaceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceRow
|
<WorkspaceRow
|
||||||
@@ -2001,7 +2052,12 @@ function WorkspaceRowItem({
|
|||||||
showShortcutBadge={showShortcutBadge}
|
showShortcutBadge={showShortcutBadge}
|
||||||
canCopyBranchName={canCopyBranchName}
|
canCopyBranchName={canCopyBranchName}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
selectionEnabled={selectionEnabled}
|
selected={isWorkspaceSelected({
|
||||||
|
selection: activeWorkspaceSelection,
|
||||||
|
serverId: workspace.serverId,
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
enabled: selectionEnabled,
|
||||||
|
})}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
drag={drag ?? noop}
|
drag={drag ?? noop}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
@@ -2010,6 +2066,39 @@ function WorkspaceRowItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function areWorkspaceRowItemPropsEqual(
|
||||||
|
previous: WorkspaceRowItemProps,
|
||||||
|
next: WorkspaceRowItemProps,
|
||||||
|
): boolean {
|
||||||
|
const previousSelected = isWorkspaceSelected({
|
||||||
|
selection: previous.activeWorkspaceSelection,
|
||||||
|
serverId: previous.workspace.serverId,
|
||||||
|
workspaceId: previous.workspace.workspaceId,
|
||||||
|
enabled: previous.selectionEnabled,
|
||||||
|
});
|
||||||
|
const nextSelected = isWorkspaceSelected({
|
||||||
|
selection: next.activeWorkspaceSelection,
|
||||||
|
serverId: next.workspace.serverId,
|
||||||
|
workspaceId: next.workspace.workspaceId,
|
||||||
|
enabled: next.selectionEnabled,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
previous.workspace === next.workspace &&
|
||||||
|
previous.shortcutNumber === next.shortcutNumber &&
|
||||||
|
previous.showShortcutBadge === next.showShortcutBadge &&
|
||||||
|
previous.canCopyBranchName === next.canCopyBranchName &&
|
||||||
|
previous.isCreating === next.isCreating &&
|
||||||
|
previous.serverId === next.serverId &&
|
||||||
|
previous.onWorkspacePress === next.onWorkspacePress &&
|
||||||
|
previous.drag === next.drag &&
|
||||||
|
previous.isDragging === next.isDragging &&
|
||||||
|
previous.dragHandleProps === next.dragHandleProps &&
|
||||||
|
previousSelected === nextSelected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoWorkspaceRowItem = memo(WorkspaceRowItem, areWorkspaceRowItemPropsEqual);
|
||||||
|
|
||||||
function WorkspaceRow({
|
function WorkspaceRow({
|
||||||
workspace,
|
workspace,
|
||||||
shortcutNumber,
|
shortcutNumber,
|
||||||
@@ -2020,7 +2109,7 @@ function WorkspaceRow({
|
|||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
canCopyBranchName,
|
canCopyBranchName,
|
||||||
isCreating = false,
|
isCreating = false,
|
||||||
selectionEnabled,
|
selected,
|
||||||
}: {
|
}: {
|
||||||
workspace: SidebarWorkspaceEntry;
|
workspace: SidebarWorkspaceEntry;
|
||||||
shortcutNumber: number | null;
|
shortcutNumber: number | null;
|
||||||
@@ -2031,14 +2120,9 @@ function WorkspaceRow({
|
|||||||
dragHandleProps?: DraggableListDragHandleProps;
|
dragHandleProps?: DraggableListDragHandleProps;
|
||||||
canCopyBranchName: boolean;
|
canCopyBranchName: boolean;
|
||||||
isCreating?: boolean;
|
isCreating?: boolean;
|
||||||
selectionEnabled: boolean;
|
selected: boolean;
|
||||||
}) {
|
}) {
|
||||||
const hydratedWorkspace = useSidebarWorkspaceEntry(workspace.serverId, workspace.workspaceId);
|
const hydratedWorkspace = useSidebarWorkspaceEntry(workspace.serverId, workspace.workspaceId);
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
|
||||||
const selected =
|
|
||||||
selectionEnabled &&
|
|
||||||
activeWorkspaceSelection?.serverId === workspace.serverId &&
|
|
||||||
activeWorkspaceSelection.workspaceId === workspace.workspaceId;
|
|
||||||
|
|
||||||
if (!hydratedWorkspace) {
|
if (!hydratedWorkspace) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2074,12 +2158,12 @@ function ProjectBlock({
|
|||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
onWorkspaceReorder,
|
onWorkspaceReorder,
|
||||||
onWorktreeCreated,
|
onWorktreeCreated,
|
||||||
currentPathname,
|
|
||||||
drag,
|
drag,
|
||||||
isDragging,
|
isDragging,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
useNestable,
|
useNestable,
|
||||||
creatingWorkspaceIds,
|
creatingWorkspaceIds,
|
||||||
|
activeWorkspaceSelection,
|
||||||
}: {
|
}: {
|
||||||
project: SidebarProjectEntry;
|
project: SidebarProjectEntry;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -2094,12 +2178,12 @@ function ProjectBlock({
|
|||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
onWorkspaceReorder: (projectKey: string, workspaces: SidebarWorkspaceEntry[]) => void;
|
onWorkspaceReorder: (projectKey: string, workspaces: SidebarWorkspaceEntry[]) => void;
|
||||||
onWorktreeCreated?: (workspaceId: string) => void;
|
onWorktreeCreated?: (workspaceId: string) => void;
|
||||||
currentPathname: string | null;
|
|
||||||
drag: () => void;
|
drag: () => void;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
dragHandleProps?: DraggableListDragHandleProps;
|
dragHandleProps?: DraggableListDragHandleProps;
|
||||||
useNestable: boolean;
|
useNestable: boolean;
|
||||||
creatingWorkspaceIds: ReadonlySet<string>;
|
creatingWorkspaceIds: ReadonlySet<string>;
|
||||||
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||||
}) {
|
}) {
|
||||||
const rowModel = useMemo(
|
const rowModel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -2110,15 +2194,12 @@ function ProjectBlock({
|
|||||||
[collapsed, project],
|
[collapsed, project],
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectWorkspaceIds = useMemo(
|
const active = isProjectSelectedByRoute({
|
||||||
() => project.workspaces.map((workspace) => workspace.workspaceId),
|
selection: activeWorkspaceSelection,
|
||||||
[project.workspaces],
|
serverId,
|
||||||
);
|
project,
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
enabled: selectionEnabled,
|
||||||
const isProjectActive =
|
});
|
||||||
selectionEnabled &&
|
|
||||||
activeWorkspaceSelection?.serverId === serverId &&
|
|
||||||
projectWorkspaceIds.includes(activeWorkspaceSelection.workspaceId);
|
|
||||||
|
|
||||||
const renderWorkspaceRow = useCallback(
|
const renderWorkspaceRow = useCallback(
|
||||||
(
|
(
|
||||||
@@ -2130,7 +2211,7 @@ function ProjectBlock({
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<WorkspaceRowItem
|
<MemoWorkspaceRowItem
|
||||||
workspace={item}
|
workspace={item}
|
||||||
shortcutNumber={shortcutIndexByWorkspaceKey.get(item.workspaceKey) ?? null}
|
shortcutNumber={shortcutIndexByWorkspaceKey.get(item.workspaceKey) ?? null}
|
||||||
showShortcutBadge={showShortcutBadges}
|
showShortcutBadge={showShortcutBadges}
|
||||||
@@ -2138,7 +2219,7 @@ function ProjectBlock({
|
|||||||
isCreating={creatingWorkspaceIds.has(item.workspaceId)}
|
isCreating={creatingWorkspaceIds.has(item.workspaceId)}
|
||||||
selectionEnabled={selectionEnabled}
|
selectionEnabled={selectionEnabled}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
currentPathname={currentPathname}
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
drag={input?.drag}
|
drag={input?.drag}
|
||||||
isDragging={input?.isDragging}
|
isDragging={input?.isDragging}
|
||||||
@@ -2148,8 +2229,8 @@ function ProjectBlock({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
project.projectKind,
|
project.projectKind,
|
||||||
|
activeWorkspaceSelection,
|
||||||
creatingWorkspaceIds,
|
creatingWorkspaceIds,
|
||||||
currentPathname,
|
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
serverId,
|
serverId,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
@@ -2228,10 +2309,8 @@ function ProjectBlock({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
navigateToWorkspace(serverId, flattenedRowWorkspaceId, {
|
navigateToWorkspace(serverId, flattenedRowWorkspaceId);
|
||||||
currentPathname,
|
}, [serverId, flattenedRowWorkspaceId, onWorkspacePress]);
|
||||||
});
|
|
||||||
}, [serverId, flattenedRowWorkspaceId, onWorkspacePress, currentPathname]);
|
|
||||||
|
|
||||||
const handleToggleCollapsed = useCallback(() => {
|
const handleToggleCollapsed = useCallback(() => {
|
||||||
onToggleCollapsed(project.projectKey);
|
onToggleCollapsed(project.projectKey);
|
||||||
@@ -2254,10 +2333,11 @@ function ProjectBlock({
|
|||||||
drag={drag}
|
drag={drag}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
dragHandleProps={dragHandleProps}
|
dragHandleProps={dragHandleProps}
|
||||||
isProjectActive={isProjectActive}
|
isProjectActive={active}
|
||||||
onRemoveProject={handleRemoveProject}
|
onRemoveProject={handleRemoveProject}
|
||||||
removeProjectStatus={isRemovingProject ? "pending" : "idle"}
|
removeProjectStatus={isRemovingProject ? "pending" : "idle"}
|
||||||
selectionEnabled={selectionEnabled}
|
selectionEnabled={selectionEnabled}
|
||||||
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -2271,7 +2351,7 @@ function ProjectBlock({
|
|||||||
onPress={handleToggleCollapsed}
|
onPress={handleToggleCollapsed}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
canCreateWorktree={rowModel.trailingAction === "new_worktree"}
|
canCreateWorktree={rowModel.trailingAction === "new_worktree"}
|
||||||
isProjectActive={isProjectActive}
|
isProjectActive={active}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
onWorktreeCreated={onWorktreeCreated}
|
onWorktreeCreated={onWorktreeCreated}
|
||||||
drag={drag}
|
drag={drag}
|
||||||
@@ -2303,6 +2383,46 @@ function ProjectBlock({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectBlockProps = Parameters<typeof ProjectBlock>[0];
|
||||||
|
|
||||||
|
function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlockProps): boolean {
|
||||||
|
const previousActive = isProjectSelectedByRoute({
|
||||||
|
selection: previous.activeWorkspaceSelection,
|
||||||
|
project: previous.project,
|
||||||
|
serverId: previous.serverId,
|
||||||
|
enabled: previous.selectionEnabled,
|
||||||
|
});
|
||||||
|
const nextActive = isProjectSelectedByRoute({
|
||||||
|
selection: next.activeWorkspaceSelection,
|
||||||
|
project: next.project,
|
||||||
|
serverId: next.serverId,
|
||||||
|
enabled: next.selectionEnabled,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
previous.project === next.project &&
|
||||||
|
previous.collapsed === next.collapsed &&
|
||||||
|
previous.displayName === next.displayName &&
|
||||||
|
previous.iconDataUri === next.iconDataUri &&
|
||||||
|
previous.serverId === next.serverId &&
|
||||||
|
previous.selectionEnabled === next.selectionEnabled &&
|
||||||
|
previous.showShortcutBadges === next.showShortcutBadges &&
|
||||||
|
previous.shortcutIndexByWorkspaceKey === next.shortcutIndexByWorkspaceKey &&
|
||||||
|
previous.parentGestureRef === next.parentGestureRef &&
|
||||||
|
previous.onToggleCollapsed === next.onToggleCollapsed &&
|
||||||
|
previous.onWorkspacePress === next.onWorkspacePress &&
|
||||||
|
previous.onWorkspaceReorder === next.onWorkspaceReorder &&
|
||||||
|
previous.onWorktreeCreated === next.onWorktreeCreated &&
|
||||||
|
previous.drag === next.drag &&
|
||||||
|
previous.isDragging === next.isDragging &&
|
||||||
|
previous.dragHandleProps === next.dragHandleProps &&
|
||||||
|
previous.useNestable === next.useNestable &&
|
||||||
|
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
||||||
|
previousActive === nextActive
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoProjectBlock = memo(ProjectBlock, areProjectBlockPropsEqual);
|
||||||
|
|
||||||
export function SidebarWorkspaceList({
|
export function SidebarWorkspaceList({
|
||||||
projects,
|
projects,
|
||||||
serverId,
|
serverId,
|
||||||
@@ -2333,6 +2453,7 @@ export function SidebarWorkspaceList({
|
|||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
const selectionEnabled = isWorkspaceRoute;
|
const selectionEnabled = isWorkspaceRoute;
|
||||||
|
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||||
|
|
||||||
const projectIconRequests = useMemo(() => {
|
const projectIconRequests = useMemo(() => {
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
@@ -2374,6 +2495,12 @@ export function SidebarWorkspaceList({
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectIconSignature = projectIconQueries.map((query) => query.data ?? "").join("\u0000");
|
||||||
|
const projectIconData = useStableProjectIconData(
|
||||||
|
projectIconQueries.map((query) => query.data ?? null),
|
||||||
|
projectIconSignature,
|
||||||
|
);
|
||||||
|
|
||||||
const projectIconByProjectKey = useMemo(() => {
|
const projectIconByProjectKey = useMemo(() => {
|
||||||
const iconByServerAndCwd = new Map<string, string | null>();
|
const iconByServerAndCwd = new Map<string, string | null>();
|
||||||
for (let index = 0; index < projectIconRequests.length; index += 1) {
|
for (let index = 0; index < projectIconRequests.length; index += 1) {
|
||||||
@@ -2381,10 +2508,7 @@ export function SidebarWorkspaceList({
|
|||||||
if (!request) {
|
if (!request) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
iconByServerAndCwd.set(
|
iconByServerAndCwd.set(`${request.serverId}:${request.cwd}`, projectIconData[index] ?? null);
|
||||||
`${request.serverId}:${request.cwd}`,
|
|
||||||
projectIconQueries[index]?.data ?? null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const byProject = new Map<string, string | null>();
|
const byProject = new Map<string, string | null>();
|
||||||
@@ -2398,7 +2522,7 @@ export function SidebarWorkspaceList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return byProject;
|
return byProject;
|
||||||
}, [projectIconQueries, projectIconRequests, projects, serverId]);
|
}, [projectIconData, projectIconRequests, projects, serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeouts = creatingWorkspaceTimeoutsRef.current;
|
const timeouts = creatingWorkspaceTimeoutsRef.current;
|
||||||
@@ -2532,7 +2656,7 @@ export function SidebarWorkspaceList({
|
|||||||
const renderProject = useCallback(
|
const renderProject = useCallback(
|
||||||
({ item, drag, isActive, dragHandleProps }: DraggableRenderItemInfo<SidebarProjectEntry>) => {
|
({ item, drag, isActive, dragHandleProps }: DraggableRenderItemInfo<SidebarProjectEntry>) => {
|
||||||
return (
|
return (
|
||||||
<ProjectBlock
|
<MemoProjectBlock
|
||||||
project={item}
|
project={item}
|
||||||
collapsed={collapsedProjectKeys.has(item.projectKey)}
|
collapsed={collapsedProjectKeys.has(item.projectKey)}
|
||||||
displayName={item.projectName}
|
displayName={item.projectName}
|
||||||
@@ -2546,23 +2670,23 @@ export function SidebarWorkspaceList({
|
|||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
onWorkspaceReorder={handleWorkspaceReorder}
|
onWorkspaceReorder={handleWorkspaceReorder}
|
||||||
onWorktreeCreated={handleWorktreeCreated}
|
onWorktreeCreated={handleWorktreeCreated}
|
||||||
currentPathname={pathname}
|
|
||||||
drag={drag}
|
drag={drag}
|
||||||
isDragging={isActive}
|
isDragging={isActive}
|
||||||
dragHandleProps={dragHandleProps}
|
dragHandleProps={dragHandleProps}
|
||||||
useNestable={platformIsNative}
|
useNestable={platformIsNative}
|
||||||
creatingWorkspaceIds={creatingWorkspaceIds}
|
creatingWorkspaceIds={creatingWorkspaceIds}
|
||||||
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
collapsedProjectKeys,
|
collapsedProjectKeys,
|
||||||
|
activeWorkspaceSelection,
|
||||||
handleWorktreeCreated,
|
handleWorktreeCreated,
|
||||||
handleWorkspaceReorder,
|
handleWorkspaceReorder,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
onToggleProjectCollapsed,
|
onToggleProjectCollapsed,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
pathname,
|
|
||||||
projectIconByProjectKey,
|
projectIconByProjectKey,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
serverId,
|
serverId,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
type WorkspaceLayout,
|
type WorkspaceLayout,
|
||||||
} from "@/stores/workspace-layout-store";
|
} from "@/stores/workspace-layout-store";
|
||||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||||
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
|
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
|
||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
|
|
||||||
@@ -78,7 +79,6 @@ interface SplitContainerProps {
|
|||||||
isWorkspaceFocused: boolean;
|
isWorkspaceFocused: boolean;
|
||||||
uiTabs: WorkspaceTab[];
|
uiTabs: WorkspaceTab[];
|
||||||
hoveredCloseTabKey: string | null;
|
hoveredCloseTabKey: string | null;
|
||||||
setHoveredTabKey: Dispatch<SetStateAction<string | null>>;
|
|
||||||
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
||||||
closingTabIds: Set<string>;
|
closingTabIds: Set<string>;
|
||||||
onNavigateTab: (tabId: string) => void;
|
onNavigateTab: (tabId: string) => void;
|
||||||
@@ -215,14 +215,16 @@ const MountedTabSlot = memo(function MountedTabSlot({
|
|||||||
}, [onFocusPane, paneId]);
|
}, [onFocusPane, paneId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={wrapperStyle}>
|
<RenderProfile id={`DesktopMountedTabSlot:${tabDescriptor.kind}:${tabDescriptor.tabId}`}>
|
||||||
<WorkspacePaneContent
|
<View style={wrapperStyle}>
|
||||||
content={content}
|
<WorkspacePaneContent
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
content={content}
|
||||||
isPaneFocused={isPaneFocused}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
onFocusPane={handleFocusPane}
|
isPaneFocused={isPaneFocused}
|
||||||
/>
|
onFocusPane={handleFocusPane}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
</RenderProfile>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -355,7 +357,6 @@ export function SplitContainer({
|
|||||||
isWorkspaceFocused,
|
isWorkspaceFocused,
|
||||||
uiTabs,
|
uiTabs,
|
||||||
hoveredCloseTabKey,
|
hoveredCloseTabKey,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
closingTabIds,
|
closingTabIds,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
@@ -553,63 +554,64 @@ export function SplitContainer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<RenderProfile id="SplitContainer">
|
||||||
sensors={sensors}
|
<DndContext
|
||||||
collisionDetection={dropCollisionDetection}
|
sensors={sensors}
|
||||||
onDragStart={handleDragStart}
|
collisionDetection={dropCollisionDetection}
|
||||||
onDragMove={updateDropPreview}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={updateDropPreview}
|
onDragMove={updateDropPreview}
|
||||||
onDragCancel={handleDragCancel}
|
onDragOver={updateDropPreview}
|
||||||
onDragEnd={handleDragEnd}
|
onDragCancel={handleDragCancel}
|
||||||
>
|
onDragEnd={handleDragEnd}
|
||||||
<SplitNodeView
|
>
|
||||||
node={renderRoot}
|
<SplitNodeView
|
||||||
workspaceKey={workspaceKey}
|
node={renderRoot}
|
||||||
uiTabs={uiTabs}
|
workspaceKey={workspaceKey}
|
||||||
focusedPaneId={layout.focusedPaneId}
|
uiTabs={uiTabs}
|
||||||
normalizedServerId={normalizedServerId}
|
focusedPaneId={layout.focusedPaneId}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedServerId={normalizedServerId}
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
hoveredCloseTabKey={hoveredCloseTabKey}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
closingTabIds={closingTabIds}
|
closingTabIds={closingTabIds}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
onCloseTabsToRight={onCloseTabsToRight}
|
onCloseTabsToRight={onCloseTabsToRight}
|
||||||
onCloseOtherTabs={onCloseOtherTabs}
|
onCloseOtherTabs={onCloseOtherTabs}
|
||||||
onCreateDraftTab={onCreateDraftTab}
|
onCreateDraftTab={onCreateDraftTab}
|
||||||
onCreateTerminalTab={onCreateTerminalTab}
|
onCreateTerminalTab={onCreateTerminalTab}
|
||||||
onCreateBrowserTab={onCreateBrowserTab}
|
onCreateBrowserTab={onCreateBrowserTab}
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
buildPaneContentModel={buildPaneContentModel}
|
buildPaneContentModel={buildPaneContentModel}
|
||||||
onFocusPane={onFocusPane}
|
onFocusPane={onFocusPane}
|
||||||
onSplitPane={onSplitPane}
|
onSplitPane={onSplitPane}
|
||||||
onSplitPaneEmpty={onSplitPaneEmpty}
|
onSplitPaneEmpty={onSplitPaneEmpty}
|
||||||
onResizeSplit={onResizeSplit}
|
onResizeSplit={onResizeSplit}
|
||||||
onReorderTabsInPane={onReorderTabsInPane}
|
onReorderTabsInPane={onReorderTabsInPane}
|
||||||
renderPaneEmptyState={renderPaneEmptyState}
|
renderPaneEmptyState={renderPaneEmptyState}
|
||||||
activeDragTabId={activeDragTabId}
|
activeDragTabId={activeDragTabId}
|
||||||
showDropZones={activeDragTabId !== null}
|
showDropZones={activeDragTabId !== null}
|
||||||
dropPreview={dropPreview}
|
dropPreview={dropPreview}
|
||||||
tabDropPreview={tabDropPreview}
|
tabDropPreview={tabDropPreview}
|
||||||
/>
|
/>
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay dropAnimation={null}>
|
||||||
{activeDragTabId ? (
|
{activeDragTabId ? (
|
||||||
<DragOverlayTabChip
|
<DragOverlayTabChip
|
||||||
tabId={activeDragTabId}
|
tabId={activeDragTabId}
|
||||||
uiTabs={uiTabs}
|
uiTabs={uiTabs}
|
||||||
normalizedServerId={normalizedServerId}
|
normalizedServerId={normalizedServerId}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
</RenderProfile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +713,6 @@ function SplitNodeView({
|
|||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
isWorkspaceFocused,
|
isWorkspaceFocused,
|
||||||
hoveredCloseTabKey,
|
hoveredCloseTabKey,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
closingTabIds,
|
closingTabIds,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
@@ -764,7 +765,6 @@ function SplitNodeView({
|
|||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
hoveredCloseTabKey={hoveredCloseTabKey}
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
closingTabIds={closingTabIds}
|
closingTabIds={closingTabIds}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
@@ -810,7 +810,6 @@ function SplitNodeView({
|
|||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
hoveredCloseTabKey={hoveredCloseTabKey}
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
closingTabIds={closingTabIds}
|
closingTabIds={closingTabIds}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
@@ -862,7 +861,6 @@ function SplitPaneView({
|
|||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
isWorkspaceFocused,
|
isWorkspaceFocused,
|
||||||
hoveredCloseTabKey,
|
hoveredCloseTabKey,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
closingTabIds,
|
closingTabIds,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
@@ -995,66 +993,67 @@ function SplitPaneView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View ref={paneRef} collapsable={false} style={styles.pane}>
|
<RenderProfile id={`SplitPaneView:${pane.id}`}>
|
||||||
<View style={paneTabsStyle}>
|
<View ref={paneRef} collapsable={false} style={styles.pane}>
|
||||||
<TitlebarDragRegion />
|
<View style={paneTabsStyle}>
|
||||||
<WorkspaceDesktopTabsRow
|
<TitlebarDragRegion />
|
||||||
paneId={pane.id}
|
<WorkspaceDesktopTabsRow
|
||||||
isFocused={isFocused}
|
paneId={pane.id}
|
||||||
tabs={desktopTabRowItems}
|
isFocused={isFocused}
|
||||||
normalizedServerId={normalizedServerId}
|
tabs={desktopTabRowItems}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedServerId={normalizedServerId}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
onReloadAgent={onReloadAgent}
|
onReloadAgent={onReloadAgent}
|
||||||
onRenameTab={onRenameTab}
|
onRenameTab={onRenameTab}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||||
onCloseTabsToRight={handleCloseTabsToRight}
|
onCloseTabsToRight={handleCloseTabsToRight}
|
||||||
onCloseOtherTabs={handleCloseOtherTabs}
|
onCloseOtherTabs={handleCloseOtherTabs}
|
||||||
onCreateDraftTab={onCreateDraftTab}
|
onCreateDraftTab={onCreateDraftTab}
|
||||||
onCreateTerminalTab={onCreateTerminalTab}
|
onCreateTerminalTab={onCreateTerminalTab}
|
||||||
onCreateBrowserTab={onCreateBrowserTab}
|
onCreateBrowserTab={onCreateBrowserTab}
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
onReorderTabs={handleReorderTabs}
|
onReorderTabs={handleReorderTabs}
|
||||||
onSplitRight={handleSplitRight}
|
onSplitRight={handleSplitRight}
|
||||||
onSplitDown={handleSplitDown}
|
onSplitDown={handleSplitDown}
|
||||||
externalDndContext
|
externalDndContext
|
||||||
activeDragTabId={activeDragTabId}
|
activeDragTabId={activeDragTabId}
|
||||||
tabDropPreviewIndex={
|
tabDropPreviewIndex={
|
||||||
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
|
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.paneContent}>
|
<View style={styles.paneContent}>
|
||||||
{mountedPaneTabIds.length > 0
|
{mountedPaneTabIds.length > 0
|
||||||
? mountedPaneTabIds.map((tabId) => {
|
? mountedPaneTabIds.map((tabId) => {
|
||||||
const tabDescriptor = tabDescriptorMap.get(tabId);
|
const tabDescriptor = tabDescriptorMap.get(tabId);
|
||||||
if (!tabDescriptor) {
|
if (!tabDescriptor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MountedTabSlot
|
<MountedTabSlot
|
||||||
key={tabId}
|
key={tabId}
|
||||||
tabDescriptor={tabDescriptor}
|
tabDescriptor={tabDescriptor}
|
||||||
isVisible={tabId === activeTabDescriptor?.tabId}
|
isVisible={tabId === activeTabDescriptor?.tabId}
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
isPaneFocused={isFocused && tabId === activeTabDescriptor?.tabId}
|
isPaneFocused={isFocused && tabId === activeTabDescriptor?.tabId}
|
||||||
paneId={pane.id}
|
paneId={pane.id}
|
||||||
onFocusPane={stableOnFocusPane}
|
onFocusPane={stableOnFocusPane}
|
||||||
buildPaneContentModel={buildPaneContentModel}
|
buildPaneContentModel={buildPaneContentModel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: (renderPaneEmptyState?.() ?? null)}
|
: (renderPaneEmptyState?.() ?? null)}
|
||||||
<SplitDropZone paneId={pane.id} active={showDropZones} preview={dropPreview} />
|
<SplitDropZone paneId={pane.id} active={showDropZones} preview={dropPreview} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</RenderProfile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||||
import { SquarePen } from "lucide-react-native";
|
import { SquarePen } from "lucide-react-native";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ActivityIndicator, Text, View } from "react-native";
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
import ReanimatedAnimated from "react-native-reanimated";
|
import ReanimatedAnimated from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -38,6 +38,7 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
|||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
|
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
|
||||||
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
||||||
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
|
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
|
||||||
import {
|
import {
|
||||||
type HostRuntimeConnectionStatus,
|
type HostRuntimeConnectionStatus,
|
||||||
@@ -1095,42 +1096,52 @@ function ChatAgentReadyContent({
|
|||||||
agentId,
|
agentId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
const streamSection = (
|
||||||
|
<RenderProfile id={`AgentStreamSection:${agentId}`}>
|
||||||
|
<AgentStreamSection
|
||||||
|
streamViewRef={streamViewRef}
|
||||||
|
serverId={serverId}
|
||||||
|
agentId={agentId}
|
||||||
|
agent={effectiveAgent}
|
||||||
|
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||||
|
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
||||||
|
toast={panelToast.api}
|
||||||
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
|
/>
|
||||||
|
</RenderProfile>
|
||||||
|
);
|
||||||
|
const composerSection = (
|
||||||
|
<RenderProfile id={`AgentComposerSection:${agentId}`}>
|
||||||
|
<AgentComposerSection
|
||||||
|
agentId={agentId}
|
||||||
|
serverId={serverId}
|
||||||
|
isPaneFocused={isPaneFocused}
|
||||||
|
isArchivingCurrentAgent={isArchivingCurrentAgent}
|
||||||
|
archivedAt={agentState.archivedAt}
|
||||||
|
cwd={cwd}
|
||||||
|
isSubmitLoading={false}
|
||||||
|
agentInputDraft={agentInputDraft}
|
||||||
|
onAttentionInputFocus={attentionController.clearOnInputFocus}
|
||||||
|
onAttentionPromptSend={attentionController.clearOnPromptSend}
|
||||||
|
onAddImages={handleAddImagesCallback}
|
||||||
|
onComposerHeightChange={handleComposerHeightChange}
|
||||||
|
onMessageSent={handleMessageSent}
|
||||||
|
/>
|
||||||
|
</RenderProfile>
|
||||||
|
);
|
||||||
|
const streamContent = (
|
||||||
|
<ReanimatedAnimated.View style={animatedContentStyle}>{streamSection}</ReanimatedAnimated.View>
|
||||||
|
);
|
||||||
|
const contentContainer = <View style={styles.contentContainer}>{streamContent}</View>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RewindComposerRestoreProvider text={agentInputDraft.text} setText={agentInputDraft.setText}>
|
<RewindComposerRestoreProvider text={agentInputDraft.text} setText={agentInputDraft.setText}>
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
<FileDropZone onFilesDropped={handleFilesDropped} disabled={isArchivingCurrentAgent}>
|
<FileDropZone onFilesDropped={handleFilesDropped} disabled={isArchivingCurrentAgent}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.contentContainer}>
|
{contentContainer}
|
||||||
<ReanimatedAnimated.View style={animatedContentStyle}>
|
|
||||||
<AgentStreamSection
|
|
||||||
streamViewRef={streamViewRef}
|
|
||||||
serverId={serverId}
|
|
||||||
agentId={agentId}
|
|
||||||
agent={effectiveAgent}
|
|
||||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
|
||||||
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
|
||||||
toast={panelToast.api}
|
|
||||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
|
||||||
/>
|
|
||||||
</ReanimatedAnimated.View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<AgentComposerSection
|
{composerSection}
|
||||||
agentId={agentId}
|
|
||||||
serverId={serverId}
|
|
||||||
isPaneFocused={isPaneFocused}
|
|
||||||
isArchivingCurrentAgent={isArchivingCurrentAgent}
|
|
||||||
archivedAt={agentState.archivedAt}
|
|
||||||
cwd={cwd}
|
|
||||||
isSubmitLoading={false}
|
|
||||||
agentInputDraft={agentInputDraft}
|
|
||||||
onAttentionInputFocus={attentionController.clearOnInputFocus}
|
|
||||||
onAttentionPromptSend={attentionController.clearOnPromptSend}
|
|
||||||
onAddImages={handleAddImagesCallback}
|
|
||||||
onComposerHeightChange={handleComposerHeightChange}
|
|
||||||
onMessageSent={handleMessageSent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showHistorySyncOverlay ? (
|
{showHistorySyncOverlay ? (
|
||||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||||
@@ -1158,7 +1169,7 @@ function ChatAgentReadyContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentStreamSection({
|
const AgentStreamSection = memo(function AgentStreamSection({
|
||||||
streamViewRef,
|
streamViewRef,
|
||||||
serverId,
|
serverId,
|
||||||
agentId,
|
agentId,
|
||||||
@@ -1222,7 +1233,7 @@ function AgentStreamSection({
|
|||||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function AgentComposerSection({
|
function AgentComposerSection({
|
||||||
agentId,
|
agentId,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
} from "@/screens/workspace/workspace-tab-menu";
|
} from "@/screens/workspace/workspace-tab-menu";
|
||||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
|
|
||||||
const DROPDOWN_WIDTH = 220;
|
const DROPDOWN_WIDTH = 220;
|
||||||
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
||||||
@@ -148,7 +149,6 @@ interface WorkspaceDesktopTabsRowProps {
|
|||||||
tabs: WorkspaceDesktopTabRowItem[];
|
tabs: WorkspaceDesktopTabRowItem[];
|
||||||
normalizedServerId: string;
|
normalizedServerId: string;
|
||||||
normalizedWorkspaceId: string;
|
normalizedWorkspaceId: string;
|
||||||
setHoveredTabKey: Dispatch<SetStateAction<string | null>>;
|
|
||||||
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
||||||
onNavigateTab: (tabId: string) => void;
|
onNavigateTab: (tabId: string) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -255,7 +255,6 @@ function TabChip({
|
|||||||
presentation,
|
presentation,
|
||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
resolvedTab,
|
resolvedTab,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -273,7 +272,6 @@ function TabChip({
|
|||||||
presentation: WorkspaceTabPresentation;
|
presentation: WorkspaceTabPresentation;
|
||||||
tooltipLabel: string;
|
tooltipLabel: string;
|
||||||
resolvedTab: WorkspaceDesktopTabActions;
|
resolvedTab: WorkspaceDesktopTabActions;
|
||||||
setHoveredTabKey: Dispatch<SetStateAction<string | null>>;
|
|
||||||
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
||||||
onNavigateTab: (tabId: string) => void;
|
onNavigateTab: (tabId: string) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -311,13 +309,11 @@ function TabChip({
|
|||||||
|
|
||||||
const handleTabHoverIn = useCallback(() => {
|
const handleTabHoverIn = useCallback(() => {
|
||||||
setHovered(true);
|
setHovered(true);
|
||||||
setHoveredTabKey(tab.key);
|
}, []);
|
||||||
}, [setHoveredTabKey, tab.key]);
|
|
||||||
|
|
||||||
const handleTabHoverOut = useCallback(() => {
|
const handleTabHoverOut = useCallback(() => {
|
||||||
setHovered(false);
|
setHovered(false);
|
||||||
setHoveredTabKey((current) => (current === tab.key ? null : current));
|
}, []);
|
||||||
}, [setHoveredTabKey, tab.key]);
|
|
||||||
|
|
||||||
const handleNavigateTab = useCallback(() => {
|
const handleNavigateTab = useCallback(() => {
|
||||||
onNavigateTab(tab.tabId);
|
onNavigateTab(tab.tabId);
|
||||||
@@ -328,14 +324,12 @@ function TabChip({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCloseButtonHoverIn = useCallback(() => {
|
const handleCloseButtonHoverIn = useCallback(() => {
|
||||||
setHoveredTabKey(tab.key);
|
|
||||||
setHoveredCloseTabKey(tab.key);
|
setHoveredCloseTabKey(tab.key);
|
||||||
}, [setHoveredTabKey, setHoveredCloseTabKey, tab.key]);
|
}, [setHoveredCloseTabKey, tab.key]);
|
||||||
|
|
||||||
const handleCloseButtonHoverOut = useCallback(() => {
|
const handleCloseButtonHoverOut = useCallback(() => {
|
||||||
setHoveredTabKey((current) => (current === tab.key ? null : current));
|
|
||||||
setHoveredCloseTabKey((current) => (current === tab.key ? null : current));
|
setHoveredCloseTabKey((current) => (current === tab.key ? null : current));
|
||||||
}, [setHoveredTabKey, setHoveredCloseTabKey, tab.key]);
|
}, [setHoveredCloseTabKey, tab.key]);
|
||||||
|
|
||||||
const handleCloseButtonPress = useCallback(
|
const handleCloseButtonPress = useCallback(
|
||||||
(event: { stopPropagation?: () => void }) => {
|
(event: { stopPropagation?: () => void }) => {
|
||||||
@@ -466,7 +460,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
tabs,
|
tabs,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -613,7 +606,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
resolvedTabWidth={resolvedTabWidth}
|
resolvedTabWidth={resolvedTabWidth}
|
||||||
showLabel={showLabel}
|
showLabel={showLabel}
|
||||||
showCloseButton={shouldShowCloseButton}
|
showCloseButton={shouldShowCloseButton}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
@@ -640,7 +632,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onReloadAgent,
|
onReloadAgent,
|
||||||
onRenameTab,
|
onRenameTab,
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
setHoveredTabKey,
|
|
||||||
tabDropPreviewIndex,
|
tabDropPreviewIndex,
|
||||||
tabs.length,
|
tabs.length,
|
||||||
],
|
],
|
||||||
@@ -656,7 +647,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
[layout.requiresHorizontalScrollFallback],
|
[layout.requiresHorizontalScrollFallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const row = (
|
||||||
<View
|
<View
|
||||||
style={styles.tabsContainer}
|
style={styles.tabsContainer}
|
||||||
testID="workspace-tabs-row"
|
testID="workspace-tabs-row"
|
||||||
@@ -787,8 +778,9 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
return <RenderProfile id="WorkspaceDesktopTabsRow">{row}</RenderProfile>;
|
||||||
|
}
|
||||||
function ResolvedDesktopTabChip({
|
function ResolvedDesktopTabChip({
|
||||||
item,
|
item,
|
||||||
isFocused,
|
isFocused,
|
||||||
@@ -807,7 +799,6 @@ function ResolvedDesktopTabChip({
|
|||||||
resolvedTabWidth,
|
resolvedTabWidth,
|
||||||
showLabel,
|
showLabel,
|
||||||
showCloseButton,
|
showCloseButton,
|
||||||
setHoveredTabKey,
|
|
||||||
setHoveredCloseTabKey,
|
setHoveredCloseTabKey,
|
||||||
onNavigateTab,
|
onNavigateTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
@@ -832,7 +823,6 @@ function ResolvedDesktopTabChip({
|
|||||||
resolvedTabWidth: number;
|
resolvedTabWidth: number;
|
||||||
showLabel: boolean;
|
showLabel: boolean;
|
||||||
showCloseButton: boolean;
|
showCloseButton: boolean;
|
||||||
setHoveredTabKey: Dispatch<SetStateAction<string | null>>;
|
|
||||||
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
setHoveredCloseTabKey: Dispatch<SetStateAction<string | null>>;
|
||||||
onNavigateTab: (tabId: string) => void;
|
onNavigateTab: (tabId: string) => void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
@@ -896,7 +886,6 @@ function ResolvedDesktopTabChip({
|
|||||||
presentation={presentation}
|
presentation={presentation}
|
||||||
tooltipLabel={tooltipLabel}
|
tooltipLabel={tooltipLabel}
|
||||||
resolvedTab={resolvedTab}
|
resolvedTab={resolvedTab}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
PaneProvider,
|
PaneProvider,
|
||||||
type PaneContextValue,
|
type PaneContextValue,
|
||||||
} from "@/panels/pane-context";
|
} from "@/panels/pane-context";
|
||||||
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { getPanelRegistration } from "@/panels/panel-registry";
|
import { getPanelRegistration } from "@/panels/panel-registry";
|
||||||
import { ensurePanelsRegistered } from "@/panels/register-panels";
|
import { ensurePanelsRegistered } from "@/panels/register-panels";
|
||||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||||
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
||||||
|
|
||||||
export interface WorkspacePaneContentModel {
|
export interface WorkspacePaneContentModel {
|
||||||
@@ -72,6 +74,35 @@ export function WorkspacePaneContent({
|
|||||||
onFocusPane,
|
onFocusPane,
|
||||||
}: WorkspacePaneContentProps) {
|
}: WorkspacePaneContentProps) {
|
||||||
const { Component, key, paneContextValue } = content;
|
const { Component, key, paneContextValue } = content;
|
||||||
|
const openTab = useStableEvent(paneContextValue.openTab);
|
||||||
|
const closeCurrentTab = useStableEvent(paneContextValue.closeCurrentTab);
|
||||||
|
const retargetCurrentTab = useStableEvent(paneContextValue.retargetCurrentTab);
|
||||||
|
const openFileInWorkspace = useStableEvent(paneContextValue.openFileInWorkspace);
|
||||||
|
const openImportSheet = useStableEvent(paneContextValue.openImportSheet);
|
||||||
|
const stablePaneContextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
serverId: paneContextValue.serverId,
|
||||||
|
workspaceId: paneContextValue.workspaceId,
|
||||||
|
tabId: paneContextValue.tabId,
|
||||||
|
target: paneContextValue.target,
|
||||||
|
openTab,
|
||||||
|
closeCurrentTab,
|
||||||
|
retargetCurrentTab,
|
||||||
|
openFileInWorkspace,
|
||||||
|
openImportSheet,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
closeCurrentTab,
|
||||||
|
openFileInWorkspace,
|
||||||
|
openImportSheet,
|
||||||
|
openTab,
|
||||||
|
paneContextValue.serverId,
|
||||||
|
paneContextValue.tabId,
|
||||||
|
paneContextValue.target,
|
||||||
|
paneContextValue.workspaceId,
|
||||||
|
retargetCurrentTab,
|
||||||
|
],
|
||||||
|
);
|
||||||
const paneFocusValue = useMemo(
|
const paneFocusValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createPaneFocusContextValue({
|
createPaneFocusContextValue({
|
||||||
@@ -83,10 +114,14 @@ export function WorkspacePaneContent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaneProvider value={paneContextValue}>
|
<RenderProfile
|
||||||
<PaneFocusProvider value={paneFocusValue}>
|
id={`WorkspacePaneContent:${paneContextValue.target.kind}:${paneContextValue.tabId}`}
|
||||||
<Component key={key} />
|
>
|
||||||
</PaneFocusProvider>
|
<PaneProvider value={stablePaneContextValue}>
|
||||||
</PaneProvider>
|
<PaneFocusProvider value={paneFocusValue}>
|
||||||
|
<Component key={key} />
|
||||||
|
</PaneFocusProvider>
|
||||||
|
</PaneProvider>
|
||||||
|
</RenderProfile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ import {
|
|||||||
type WorkspaceFileLocation,
|
type WorkspaceFileLocation,
|
||||||
type WorkspaceFileOpenRequest,
|
type WorkspaceFileOpenRequest,
|
||||||
} from "@/workspace/file-open";
|
} from "@/workspace/file-open";
|
||||||
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
|
|
||||||
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
||||||
const WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX = "workspace-floating-panels";
|
const WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX = "workspace-floating-panels";
|
||||||
@@ -734,16 +735,43 @@ const MobileMountedTabSlot = memo(function MobileMountedTabSlot({
|
|||||||
: styles.mobileMountedTabSlotHidden;
|
: styles.mobileMountedTabSlotHidden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={slotStyle} pointerEvents={isVisible ? "auto" : "none"}>
|
<RenderProfile id={`MobileMountedTabSlot:${tabDescriptor.kind}:${tabDescriptor.tabId}`}>
|
||||||
<WorkspacePaneContent
|
<View style={slotStyle} pointerEvents={isVisible ? "auto" : "none"}>
|
||||||
content={content}
|
<WorkspacePaneContent
|
||||||
isWorkspaceFocused={isWorkspaceFocused}
|
content={content}
|
||||||
isPaneFocused={isPaneFocused}
|
isWorkspaceFocused={isWorkspaceFocused}
|
||||||
/>
|
isPaneFocused={isPaneFocused}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
</RenderProfile>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface MobileExplorerOpenGestureSurfaceProps {
|
||||||
|
children: ReactNode;
|
||||||
|
onOpenExplorer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileExplorerOpenGestureSurface({
|
||||||
|
children,
|
||||||
|
onOpenExplorer,
|
||||||
|
}: MobileExplorerOpenGestureSurfaceProps) {
|
||||||
|
const canOpenExplorerFromAgentView = usePanelStore(
|
||||||
|
(state) =>
|
||||||
|
state.mobileView === "agent" && !selectIsFileExplorerOpen(state, { isCompact: true }),
|
||||||
|
);
|
||||||
|
const explorerOpenGesture = useExplorerOpenGesture({
|
||||||
|
enabled: canOpenExplorerFromAgentView,
|
||||||
|
onOpen: onOpenExplorer,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GestureDetector gesture={explorerOpenGesture} touchAction={COMPACT_WEB_GESTURE_TOUCH_ACTION}>
|
||||||
|
<View style={styles.content}>{children}</View>
|
||||||
|
</GestureDetector>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) {
|
function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) {
|
||||||
const cacheRef = useRef(new Map<string, WorkspaceTabDescriptor>());
|
const cacheRef = useRef(new Map<string, WorkspaceTabDescriptor>());
|
||||||
const tabDescriptorMap = useMemo(() => {
|
const tabDescriptorMap = useMemo(() => {
|
||||||
@@ -770,7 +798,11 @@ function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) {
|
|||||||
return tabDescriptorMap;
|
return tabDescriptorMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceScreen({ serverId, workspaceId, isRouteFocused }: WorkspaceScreenProps) {
|
export const WorkspaceScreen = memo(function WorkspaceScreen({
|
||||||
|
serverId,
|
||||||
|
workspaceId,
|
||||||
|
isRouteFocused,
|
||||||
|
}: WorkspaceScreenProps) {
|
||||||
const navigationFocused = useIsFocused();
|
const navigationFocused = useIsFocused();
|
||||||
const effectiveRouteFocused = isRouteFocused ?? navigationFocused;
|
const effectiveRouteFocused = isRouteFocused ?? navigationFocused;
|
||||||
|
|
||||||
@@ -783,7 +815,7 @@ export function WorkspaceScreen({ serverId, workspaceId, isRouteFocused }: Works
|
|||||||
/>
|
/>
|
||||||
</ExplorerSidebarAnimationProvider>
|
</ExplorerSidebarAnimationProvider>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface UseCloseTabsResult {
|
interface UseCloseTabsResult {
|
||||||
closingTabIds: Set<string>;
|
closingTabIds: Set<string>;
|
||||||
@@ -1657,10 +1689,6 @@ function WorkspaceScreenContent({
|
|||||||
const isExplorerOpen = usePanelStore((state) =>
|
const isExplorerOpen = usePanelStore((state) =>
|
||||||
selectIsFileExplorerOpen(state, { isCompact: isMobile }),
|
selectIsFileExplorerOpen(state, { isCompact: isMobile }),
|
||||||
);
|
);
|
||||||
const canOpenExplorerFromAgentView = usePanelStore(
|
|
||||||
(state) =>
|
|
||||||
state.mobileView === "agent" && !selectIsFileExplorerOpen(state, { isCompact: true }),
|
|
||||||
);
|
|
||||||
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
|
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
|
||||||
const toggleFileExplorerForCheckout = usePanelStore(
|
const toggleFileExplorerForCheckout = usePanelStore(
|
||||||
(state) => state.toggleFileExplorerForCheckout,
|
(state) => state.toggleFileExplorerForCheckout,
|
||||||
@@ -1712,11 +1740,6 @@ function WorkspaceScreenContent({
|
|||||||
[isExplorerOpen],
|
[isExplorerOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const explorerOpenGesture = useExplorerOpenGesture({
|
|
||||||
enabled: isMobile && canOpenExplorerFromAgentView,
|
|
||||||
onOpen: openExplorerForWorkspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRouteFocused || isWeb || !isExplorerOpen) {
|
if (!isRouteFocused || isWeb || !isExplorerOpen) {
|
||||||
return;
|
return;
|
||||||
@@ -2178,7 +2201,31 @@ function WorkspaceScreenContent({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [_hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
|
const handleOpenWorkspaceFileFromPane = useStableEvent(function handleOpenWorkspaceFileFromPane({
|
||||||
|
request,
|
||||||
|
paneId,
|
||||||
|
parentTabId,
|
||||||
|
focusPaneBeforeOpen,
|
||||||
|
}: {
|
||||||
|
request: WorkspaceFileOpenRequest;
|
||||||
|
paneId?: string | null;
|
||||||
|
parentTabId: string;
|
||||||
|
focusPaneBeforeOpen?: boolean;
|
||||||
|
}) {
|
||||||
|
if (focusPaneBeforeOpen && paneId && persistenceKey) {
|
||||||
|
focusWorkspacePane(persistenceKey, paneId);
|
||||||
|
}
|
||||||
|
if (request.disposition === "side") {
|
||||||
|
handleOpenFileFromChatInSidePane({
|
||||||
|
location: request.location,
|
||||||
|
sourcePaneId: paneId ?? undefined,
|
||||||
|
parentTabId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleOpenFileFromChat(request.location, { parentTabId });
|
||||||
|
});
|
||||||
|
|
||||||
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(null);
|
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(null);
|
||||||
const { handleRenameTab, renamingTab, handleRenameModalSubmit, handleRenameModalClose } =
|
const { handleRenameTab, renamingTab, handleRenameModalSubmit, handleRenameModalClose } =
|
||||||
useWorkspaceTabRename({
|
useWorkspaceTabRename({
|
||||||
@@ -2300,7 +2347,6 @@ function WorkspaceScreenContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeTerminalFromCache(terminalId);
|
removeTerminalFromCache(terminalId);
|
||||||
setHoveredTabKey((current) => (current === tabId ? null : current));
|
|
||||||
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
||||||
if (persistenceKey) {
|
if (persistenceKey) {
|
||||||
closeWorkspaceTabWithCleanup({
|
closeWorkspaceTabWithCleanup({
|
||||||
@@ -2349,7 +2395,6 @@ function WorkspaceScreenContent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHoveredTabKey((current) => (current === tabId ? null : current));
|
|
||||||
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
||||||
if (persistenceKey) {
|
if (persistenceKey) {
|
||||||
closeWorkspaceTabWithCleanup({
|
closeWorkspaceTabWithCleanup({
|
||||||
@@ -2374,7 +2419,6 @@ function WorkspaceScreenContent({
|
|||||||
tabId: string;
|
tabId: string;
|
||||||
target?: WorkspaceTabTarget | null;
|
target?: WorkspaceTabTarget | null;
|
||||||
}) {
|
}) {
|
||||||
setHoveredTabKey((current) => (current === input.tabId ? null : current));
|
|
||||||
setHoveredCloseTabKey((current) => (current === input.tabId ? null : current));
|
setHoveredCloseTabKey((current) => (current === input.tabId ? null : current));
|
||||||
if (persistenceKey) {
|
if (persistenceKey) {
|
||||||
closeWorkspaceTabWithCleanup({ tabId: input.tabId, target: input.target });
|
closeWorkspaceTabWithCleanup({ tabId: input.tabId, target: input.target });
|
||||||
@@ -2556,7 +2600,6 @@ function WorkspaceScreenContent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const closedKeys = new Set(tabsToClose.map((tab) => tab.key));
|
const closedKeys = new Set(tabsToClose.map((tab) => tab.key));
|
||||||
setHoveredTabKey((current) => (current && closedKeys.has(current) ? null : current));
|
|
||||||
setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current));
|
setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current));
|
||||||
},
|
},
|
||||||
[client, closeTab, closeWorkspaceTabWithCleanup, persistenceKey],
|
[client, closeTab, closeWorkspaceTabWithCleanup, persistenceKey],
|
||||||
@@ -2851,26 +2894,19 @@ function WorkspaceScreenContent({
|
|||||||
retargetWorkspaceTab(persistenceKey, input.tab.tabId, target);
|
retargetWorkspaceTab(persistenceKey, input.tab.tabId, target);
|
||||||
},
|
},
|
||||||
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => {
|
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => {
|
||||||
if (input.focusPaneBeforeOpen && input.paneId && persistenceKey) {
|
handleOpenWorkspaceFileFromPane({
|
||||||
focusWorkspacePane(persistenceKey, input.paneId);
|
request,
|
||||||
}
|
paneId: input.paneId,
|
||||||
if (request.disposition === "side") {
|
parentTabId: input.tab.tabId,
|
||||||
handleOpenFileFromChatInSidePane({
|
focusPaneBeforeOpen: input.focusPaneBeforeOpen,
|
||||||
location: request.location,
|
});
|
||||||
sourcePaneId: input.paneId ?? undefined,
|
|
||||||
parentTabId: input.tab.tabId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleOpenFileFromChat(request.location, { parentTabId: input.tab.tabId });
|
|
||||||
},
|
},
|
||||||
onOpenImportSheet: openImportSheet,
|
onOpenImportSheet: openImportSheet,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
handleCloseTabById,
|
handleCloseTabById,
|
||||||
handleOpenFileFromChat,
|
|
||||||
handleOpenFileFromChatInSidePane,
|
|
||||||
focusWorkspacePane,
|
focusWorkspacePane,
|
||||||
|
handleOpenWorkspaceFileFromPane,
|
||||||
navigateToTabId,
|
navigateToTabId,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
@@ -3193,9 +3229,9 @@ function WorkspaceScreenContent({
|
|||||||
`${WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX}:${normalizedServerId}:${normalizedWorkspaceId}`,
|
`${WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX}:${normalizedServerId}:${normalizedWorkspaceId}`,
|
||||||
[normalizedServerId, normalizedWorkspaceId],
|
[normalizedServerId, normalizedWorkspaceId],
|
||||||
);
|
);
|
||||||
const desktopContent = useMemo(() => {
|
const desktopSplitContent = useMemo(() => {
|
||||||
if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) {
|
if (!canRenderDesktopPaneSplits || !workspaceLayout || !persistenceKey) {
|
||||||
return content;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SplitContainer
|
<SplitContainer
|
||||||
@@ -3207,7 +3243,6 @@ function WorkspaceScreenContent({
|
|||||||
isWorkspaceFocused={isRouteFocused}
|
isWorkspaceFocused={isRouteFocused}
|
||||||
uiTabs={uiTabs}
|
uiTabs={uiTabs}
|
||||||
hoveredCloseTabKey={hoveredCloseTabKey}
|
hoveredCloseTabKey={hoveredCloseTabKey}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
closingTabIds={closingTabIds}
|
closingTabIds={closingTabIds}
|
||||||
onNavigateTab={navigateToTabId}
|
onNavigateTab={navigateToTabId}
|
||||||
@@ -3234,7 +3269,6 @@ function WorkspaceScreenContent({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
content,
|
|
||||||
canRenderDesktopPaneSplits,
|
canRenderDesktopPaneSplits,
|
||||||
workspaceLayout,
|
workspaceLayout,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
@@ -3267,6 +3301,7 @@ function WorkspaceScreenContent({
|
|||||||
handleReorderTabsInPane,
|
handleReorderTabsInPane,
|
||||||
renderSplitPaneEmptyState,
|
renderSplitPaneEmptyState,
|
||||||
]);
|
]);
|
||||||
|
const desktopContent = desktopSplitContent ?? content;
|
||||||
|
|
||||||
const workspaceCenterColumn = (
|
const workspaceCenterColumn = (
|
||||||
<View style={styles.centerColumn}>
|
<View style={styles.centerColumn}>
|
||||||
@@ -3343,7 +3378,6 @@ function WorkspaceScreenContent({
|
|||||||
tabs={desktopTabRowItems}
|
tabs={desktopTabRowItems}
|
||||||
normalizedServerId={normalizedServerId}
|
normalizedServerId={normalizedServerId}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
setHoveredTabKey={setHoveredTabKey}
|
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
onNavigateTab={navigateToTabId}
|
onNavigateTab={navigateToTabId}
|
||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
@@ -3369,12 +3403,9 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
<View style={styles.centerContent}>
|
<View style={styles.centerContent}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<GestureDetector
|
<MobileExplorerOpenGestureSurface onOpenExplorer={openExplorerForWorkspace}>
|
||||||
gesture={explorerOpenGesture}
|
{content}
|
||||||
touchAction={COMPACT_WEB_GESTURE_TOUCH_ACTION}
|
</MobileExplorerOpenGestureSurface>
|
||||||
>
|
|
||||||
<View style={styles.content}>{content}</View>
|
|
||||||
</GestureDetector>
|
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.content}>{desktopContent}</View>
|
<View style={styles.content}>{desktopContent}</View>
|
||||||
)}
|
)}
|
||||||
@@ -3385,44 +3416,46 @@ function WorkspaceScreenContent({
|
|||||||
return (
|
return (
|
||||||
gatedWorkspaceScreen ?? (
|
gatedWorkspaceScreen ?? (
|
||||||
<WorkspaceFocusProvider workspaceKey={persistenceKey}>
|
<WorkspaceFocusProvider workspaceKey={persistenceKey}>
|
||||||
<View style={containerStyle}>
|
<RenderProfile id="WorkspaceScreenContent">
|
||||||
<WorkspaceDocumentTitleEffectSlot
|
<View style={containerStyle}>
|
||||||
tab={activeTabDescriptor}
|
<WorkspaceDocumentTitleEffectSlot
|
||||||
serverId={normalizedServerId}
|
tab={activeTabDescriptor}
|
||||||
workspaceId={normalizedWorkspaceId}
|
serverId={normalizedServerId}
|
||||||
isRouteFocused={isRouteFocused}
|
workspaceId={normalizedWorkspaceId}
|
||||||
/>
|
isRouteFocused={isRouteFocused}
|
||||||
<View style={styles.threePaneRow}>
|
/>
|
||||||
<FloatingPanelPortalHostNameProvider hostName={workspaceFloatingPanelPortalHostName}>
|
<View style={styles.threePaneRow}>
|
||||||
{workspaceCenterColumn}
|
<FloatingPanelPortalHostNameProvider hostName={workspaceFloatingPanelPortalHostName}>
|
||||||
</FloatingPanelPortalHostNameProvider>
|
{workspaceCenterColumn}
|
||||||
|
</FloatingPanelPortalHostNameProvider>
|
||||||
|
|
||||||
<FloatingPanelPortalHost name={workspaceFloatingPanelPortalHostName} />
|
<FloatingPanelPortalHost name={workspaceFloatingPanelPortalHostName} />
|
||||||
|
|
||||||
{showExplorerSidebar && workspaceDirectory ? (
|
{showExplorerSidebar && workspaceDirectory ? (
|
||||||
<ExplorerSidebar
|
<ExplorerSidebar
|
||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
workspaceId={normalizedWorkspaceId}
|
workspaceId={normalizedWorkspaceId}
|
||||||
workspaceRoot={workspaceDirectory}
|
workspaceRoot={workspaceDirectory}
|
||||||
isGit={isGitCheckout}
|
isGit={isGitCheckout}
|
||||||
onOpenFile={handleOpenFileFromExplorer}
|
onOpenFile={handleOpenFileFromExplorer}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
</View>
|
||||||
|
<ImportSessionSheet
|
||||||
|
visible={isImportSheetVisible}
|
||||||
|
client={client}
|
||||||
|
serverId={normalizedServerId}
|
||||||
|
cwd={workspaceDirectory}
|
||||||
|
onClose={closeImportSheet}
|
||||||
|
onImportedAgent={handleImportedAgent}
|
||||||
|
/>
|
||||||
|
<WorkspaceTabRenameModal
|
||||||
|
renamingTab={renamingTab}
|
||||||
|
onSubmit={handleRenameModalSubmit}
|
||||||
|
onClose={handleRenameModalClose}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ImportSessionSheet
|
</RenderProfile>
|
||||||
visible={isImportSheetVisible}
|
|
||||||
client={client}
|
|
||||||
serverId={normalizedServerId}
|
|
||||||
cwd={workspaceDirectory}
|
|
||||||
onClose={closeImportSheet}
|
|
||||||
onImportedAgent={handleImportedAgent}
|
|
||||||
/>
|
|
||||||
<WorkspaceTabRenameModal
|
|
||||||
renamingTab={renamingTab}
|
|
||||||
onSubmit={handleRenameModalSubmit}
|
|
||||||
onClose={handleRenameModalClose}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</WorkspaceFocusProvider>
|
</WorkspaceFocusProvider>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Profiler, type ProfilerOnRenderCallback, type ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface RenderProfileSample {
|
||||||
|
id: string;
|
||||||
|
phase: "mount" | "update" | "nested-update";
|
||||||
|
actualDuration: number;
|
||||||
|
baseDuration: number;
|
||||||
|
startTime: number;
|
||||||
|
commitTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var __PASEO_RENDER_PROFILE__: RenderProfileSample[] | undefined;
|
||||||
|
var __PASEO_RENDER_PROFILE_REASONS__: Record<string, Record<string, number>> | undefined;
|
||||||
|
var __PASEO_RESET_RENDER_PROFILE__: (() => void) | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchParam(name: string): string | null {
|
||||||
|
const location = Reflect.get(globalThis, "location");
|
||||||
|
if (!location || typeof location !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const search = Reflect.get(location, "search");
|
||||||
|
if (typeof search !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new URLSearchParams(search).get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRenderProfileEnabled(): boolean {
|
||||||
|
return getSearchParam("renderProfile") === "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRender: ProfilerOnRenderCallback = (
|
||||||
|
id,
|
||||||
|
phase,
|
||||||
|
actualDuration,
|
||||||
|
baseDuration,
|
||||||
|
startTime,
|
||||||
|
commitTime,
|
||||||
|
) => {
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE__ ??= [];
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE__.push({
|
||||||
|
id,
|
||||||
|
phase,
|
||||||
|
actualDuration,
|
||||||
|
baseDuration,
|
||||||
|
startTime,
|
||||||
|
commitTime,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RenderProfile({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
if (!isRenderProfileEnabled()) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE__ ??= [];
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE_REASONS__ ??= {};
|
||||||
|
globalThis.__PASEO_RESET_RENDER_PROFILE__ = () => {
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE__ = [];
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE_REASONS__ = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Profiler id={id} onRender={onRender}>
|
||||||
|
{children}
|
||||||
|
</Profiler>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordRenderProfileReasons(id: string, reasons: string[]) {
|
||||||
|
if (!isRenderProfileEnabled() || reasons.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalThis.__PASEO_RENDER_PROFILE_REASONS__ ??= {};
|
||||||
|
const counts = (globalThis.__PASEO_RENDER_PROFILE_REASONS__[id] ??= {});
|
||||||
|
for (const reason of reasons) {
|
||||||
|
counts[reason] = (counts[reason] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user